Parcial Práctico - Curso NLP¶

El presente trabajo presenta la solución a los ejercicios 1 a 3 del parcial práctico del curso electivo en Natural language processing (NLP). Adicionalmente, se encuentra organizado en tres secciones correspondientes a cada uno de los ejercicios propuestos.

Antes de iniciar cargamos todas las librerias necesarias para desarrollar el parcial práctico:

In [7]:
!pip install scikit-optimize
!pip install xgboost
!pip install ace_tools
Requirement already satisfied: scikit-optimize in /usr/local/lib/python3.11/dist-packages (0.10.2)
Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.11/dist-packages (from scikit-optimize) (1.5.1)
Requirement already satisfied: pyaml>=16.9 in /usr/local/lib/python3.11/dist-packages (from scikit-optimize) (25.7.0)
Requirement already satisfied: numpy>=1.20.3 in /usr/local/lib/python3.11/dist-packages (from scikit-optimize) (2.0.2)
Requirement already satisfied: scipy>=1.1.0 in /usr/local/lib/python3.11/dist-packages (from scikit-optimize) (1.16.0)
Requirement already satisfied: scikit-learn>=1.0.0 in /usr/local/lib/python3.11/dist-packages (from scikit-optimize) (1.6.1)
Requirement already satisfied: packaging>=21.3 in /usr/local/lib/python3.11/dist-packages (from scikit-optimize) (25.0)
Requirement already satisfied: PyYAML in /usr/local/lib/python3.11/dist-packages (from pyaml>=16.9->scikit-optimize) (6.0.2)
Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.11/dist-packages (from scikit-learn>=1.0.0->scikit-optimize) (3.6.0)
Requirement already satisfied: xgboost in /usr/local/lib/python3.11/dist-packages (3.0.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.11/dist-packages (from xgboost) (2.0.2)
Requirement already satisfied: nvidia-nccl-cu12 in /usr/local/lib/python3.11/dist-packages (from xgboost) (2.21.5)
Requirement already satisfied: scipy in /usr/local/lib/python3.11/dist-packages (from xgboost) (1.16.0)
Collecting ace_tools
  Downloading ace_tools-0.0-py3-none-any.whl.metadata (300 bytes)
Downloading ace_tools-0.0-py3-none-any.whl (1.1 kB)
Installing collected packages: ace_tools
Successfully installed ace_tools-0.0
In [11]:
# Manipulación y análisis de datos
import numpy as np
import pandas as pd
import xgboost as xgb

# Visualización
import matplotlib.pyplot as plt
import seaborn as sns
import altair as alt

# Modelado y preprocesamiento
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.preprocessing import StandardScaler, LabelEncoder, MinMaxScaler, OneHotEncoder
from sklearn.pipeline import Pipeline

# Clasificadores
from sklearn.linear_model import LogisticRegression, LinearRegression, RidgeCV, LassoCV, Ridge, Lasso
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.compose import ColumnTransformer
from sklearn.svm import SVC, SVR
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.neural_network import MLPClassifier, MLPRegressor
from sklearn.impute import SimpleImputer
from xgboost import XGBClassifier, XGBRegressor
from statsmodels.stats.outliers_influence import variance_inflation_factor

# Métricas
from sklearn.metrics import (
    classification_report, confusion_matrix,
    precision_score, recall_score, f1_score, roc_auc_score,
    RocCurveDisplay, mean_absolute_error, mean_squared_error
)
from statsmodels.stats.diagnostic import acorr_ljungbox

# Optimización de hiperparámetros
from skopt import BayesSearchCV
from skopt.space import Real, Integer, Categorical

# Redes Neuronales
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import SimpleRNN, LSTM, Dense
from tensorflow.keras.optimizers import Adam
from tensorflow.keras import Input

Ejercicio No. 1: Análisis Exploratorio de Datos¶

Dataset: Velocidad del Viento¶

a. Descripción de tipos de variables¶
In [ ]:
# Cargamos el primer dataset de velocidad del viento
url = "https://raw.githubusercontent.com/lihkir/Data/main/wind_speed/data_treino_dv_df_2000_2010.csv"
df = pd.read_csv(url)

# Mostrar una descripción rápida del dataframe
df.info()

# Mostrar las primeras filas del dataset
df.head()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 87693 entries, 0 to 87692
Data columns (total 13 columns):
 #   Column                                                 Non-Null Count  Dtype  
---  ------                                                 --------------  -----  
 0   HORA (UTC)                                             87693 non-null  object 
 1   VENTO, DIRE��O HORARIA (gr) (� (gr))             87693 non-null  float64
 2   VENTO, VELOCIDADE HORARIA (m/s)                        87693 non-null  float64
 3   UMIDADE REL. MAX. NA HORA ANT. (AUT) (%)               87693 non-null  float64
 4   UMIDADE REL. MIN. NA HORA ANT. (AUT) (%)               87693 non-null  float64
 5   TEMPERATURA M�XIMA NA HORA ANT. (AUT) (�C)         87693 non-null  float64
 6   TEMPERATURA M�NIMA NA HORA ANT. (AUT) (�C)         87693 non-null  float64
 7   UMIDADE RELATIVA DO AR, HORARIA (%)                    87693 non-null  float64
 8   PRESSAO ATMOSFERICA AO NIVEL DA ESTACAO, HORARIA (mB)  87693 non-null  float64
 9   PRECIPITA��O TOTAL, HOR�RIO (mm)                 87693 non-null  float64
 10  VENTO, RAJADA MAXIMA (m/s)                             87693 non-null  float64
 11  PRESS�O ATMOSFERICA MAX.NA HORA ANT. (AUT) (mB)      87693 non-null  float64
 12  PRESS�O ATMOSFERICA MIN. NA HORA ANT. (AUT) (mB)     87693 non-null  float64
dtypes: float64(12), object(1)
memory usage: 8.7+ MB
Out[ ]:
HORA (UTC) VENTO, DIRE��O HORARIA (gr) (� (gr)) VENTO, VELOCIDADE HORARIA (m/s) UMIDADE REL. MAX. NA HORA ANT. (AUT) (%) UMIDADE REL. MIN. NA HORA ANT. (AUT) (%) TEMPERATURA M�XIMA NA HORA ANT. (AUT) (�C) TEMPERATURA M�NIMA NA HORA ANT. (AUT) (�C) UMIDADE RELATIVA DO AR, HORARIA (%) PRESSAO ATMOSFERICA AO NIVEL DA ESTACAO, HORARIA (mB) PRECIPITA��O TOTAL, HOR�RIO (mm) VENTO, RAJADA MAXIMA (m/s) PRESS�O ATMOSFERICA MAX.NA HORA ANT. (AUT) (mB) PRESS�O ATMOSFERICA MIN. NA HORA ANT. (AUT) (mB)
0 12:00 0.809017 1.8 69.0 60.0 22.6 20.7 61.0 888.2 0.0 3.8 888.2 887.7
1 13:00 0.965926 2.7 62.0 55.0 24.2 22.5 55.0 888.4 0.0 4.7 888.4 888.2
2 14:00 0.891007 2.0 56.0 50.0 25.5 24.3 51.0 888.1 0.0 4.9 888.4 888.1
3 15:00 0.848048 2.5 52.0 44.0 27.4 25.0 44.0 887.4 0.0 5.8 888.1 887.4
4 16:00 0.224951 2.4 50.0 43.0 27.1 25.5 46.0 886.5 0.0 5.8 887.4 886.5

El dataframe contiene 87.693 registros con infromación meteorológica horaria, los cuales se encuentran distribuidos en 13 variables. Entre las variables de incluyen datos sobre la hora (formato texto), la dirección y velocidad del viento, los niveles de humedad relativa (máxima, mínima, y horaria), temperaturas extremas registradas en la hora anterior, presión atmosférica (instantánea, máxima, y mínima en la hora anterior), precipitaciones horarias y la velocidad de la racha máxima de viento. A excepción de la columna HORA (UTC), todos los datos son númericos de tipo float y no presentan valores faltantes, lo que sugiere un conjunto de datos completo.

Tenemos las siguientes variables con su definición:

  • HORA (UTC): Hora
  • VENTO, DIRE��O HORARIA (gr) (� (gr)): Dirección del viento horaria
  • VENTO, VELOCIDADE HORARIA (m/s): Velocidad horario del viento (m/s)
  • UMIDADE REL. MAX. NA HORA ANT. (AUT) ( %): Humedad rel. máx. hora anterior (AUT) ( %)
  • UMIDADE REL. MIN. NA HORA ANT. (AUT) ( %): Humedad rel. mín. hora anterior (AUT) ( %)
  • TEMPERATURA M�XIMA NA HORA ANT. (AUT) (�C): Temperatura máx. hora anterior (AUT)(℃)
  • TEMPERATURA M�NIMA NA HORA ANT. (AUT) (�C): Temperatura mín. hora anterior (AUT)( ℃)
  • UMIDADE RELATIVA DO AR, HORARIA (%): Humedad relativa horaria (%)
  • PRESSAO ATMOSFERICA AO NIVEL DA ESTACAO, HORARIA (mB): Presión atmosférica a nivel de estación, horaria (mB)
  • PRECIPITA��O TOTAL, HOR�RIO (mm): Precipitación total por hora (mm)
  • VENTO, RAJADA MAXIMA (m/s): Máxima ráfaga de viento (m/s)
  • PRESS�O ATMOSFERICA MAX.NA HORA ANT. (AUT)(mB): Presión atmosférica máx. hora anterior (AUT) (mB)
  • PRESS�O ATMOSFERICA MIN.NA HORA ANT. (AUT)(mB): Presión atmosférica mín. hora anterior (AUT)(mB)
b. Reducción de nombres extensos en columnas¶

Dado que los nombres actuales están en portugués, son extensos y contienen caracteres especiales, para facilidad de análisis los renombraremos:

In [ ]:
# Renombramos las variables para facilitar el análisis
df.columns = [
    'hora',
    'vento_direcao',
    'vento_velocidade',
    'umid_max_ant',
    'umid_min_ant',
    'temp_max_ant',
    'temp_min_ant',
    'umid_horaria',
    'pressao_horaria',
    'precipitacao',
    'vento_rajada_max',
    'pressao_max_ant',
    'pressao_min_ant'
]

Con nombres más simples, procedemos a ver un resumen estadístico de los datos y revisar si hay datos faltantes

c. Conteo de datos faltantes y su porcentaje¶

In [ ]:
# Conteo y porcentaje de datos faltantes
missing_count = df.isnull().sum()
missing_percent = (missing_count / len(df)) * 100
missing_data = pd.DataFrame({
    'missing_count': missing_count,
    'missing_percent': missing_percent
})
print(missing_data)
                  missing_count  missing_percent
hora                          0              0.0
vento_direcao                 0              0.0
vento_velocidade              0              0.0
umid_max_ant                  0              0.0
umid_min_ant                  0              0.0
temp_max_ant                  0              0.0
temp_min_ant                  0              0.0
umid_horaria                  0              0.0
pressao_horaria               0              0.0
precipitacao                  0              0.0
vento_rajada_max              0              0.0
pressao_max_ant               0              0.0
pressao_min_ant               0              0.0

El resumen muestra que el conjunto de datos no contiene valores faltantes en ninguna de las 13 variables existentes. Tanto el conteo de valores faltantes (missing_count) como su porcentaje (missing_percent) son cero para todas las variables. Esto indica que el dataset esta completo y no requiere imputación de datos faltantes.

d. Calcular variables descriptivas¶

Utilizamos la función describe para calcular un resumen estadístico que contine el número de observaciones, la media, la desviación estándar, mínimo y máximo, y los cuartiles.

In [ ]:
# Resumen estadístico
df.describe()
Out[ ]:
vento_direcao vento_velocidade umid_max_ant umid_min_ant temp_max_ant temp_min_ant umid_horaria pressao_horaria precipitacao vento_rajada_max pressao_max_ant pressao_min_ant
count 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000
mean 0.405810 2.466192 69.058465 63.176194 21.921264 20.684570 66.146682 887.251925 0.160907 5.161076 887.580724 886.891093
std 0.686247 1.313968 19.640222 20.166336 3.721386 3.513744 19.992327 4.012404 1.307515 2.311157 3.646750 3.564539
min -1.000000 0.000000 12.000000 10.000000 9.200000 8.400000 10.000000 863.400000 0.000000 0.000000 865.300000 862.800000
25% -0.156434 1.500000 54.000000 48.000000 19.200000 18.400000 51.000000 885.300000 0.000000 3.400000 885.600000 885.000000
50% 0.788011 2.400000 72.000000 64.000000 21.400000 20.200000 68.000000 887.200000 0.000000 5.000000 887.500000 886.900000
75% 0.970296 3.400000 87.000000 80.000000 24.700000 23.100000 84.000000 889.100000 0.000000 6.800000 889.300000 888.800000
max 1.000000 10.000000 100.000000 98.000000 35.300000 34.400000 99.000000 1023.500000 70.800000 24.300000 913.100000 910.900000

La tabla describe las principales estadísticas de las 12 variables meteorológicas cuantitativas que contiene la base de datos. Podemos observar que:

  • 🌬️Viento
    • La dirección del viento (vento_direcao) tiene un rango entre -1 y 1, con media cercana a 0.41.
    • La velocidad del viento (vento_velocidade) promedio es aproximadamente 2.47 m/s, con un máximo registrado de 10 m/s.
  • 💧Humedad
    • La humedad relativa máxima (umid_max_ant) y mínima (umid_min_ant) de la hora anterior y horaria (umid_horaria) varia entre 10% y 100%, con una media general alrededor de 66%.
  • 🌡️Temperatura
    • Las temperatura mínima (temp_min_ant) en la hora anterior registrada es de 8.4°C, mientras que la temperatura máxima (temp_max_ant) es de 35.3°C.
  • 🧭Presión atmosférica
    • La presión atmosférica horaria (pressao_horaria) promedio es de 887.25 mB, con un rango entre 863.4 mB y 1023.5 mB.
  • 🌧️Precipitación
  • La precipitación tiene una media de 0.16 mm. Esta media es baja en comparación con el valor máximo de 10.8 mm. Lo que puede inidicar que los episodios de lluvia son pocos pero bastante intensos.
  • El 75% de los valores son 0 mm, lo que sugiere que en la mayoría de las horas no llueve.

e. Histogramas & Diagramas de caja y bigotes¶

En esta sección analizaremos la distribución de las variables de nuestro modelo. Para esto, realizaremos histogramas con el fin de ver la distribución de los datos. Adicionalmente, utilizaremos diagramas de caja y bigotes para identificar datos atípicos dentro de las variables. Las mediciones de skewness y kurtosis, nos ayudan a identificar la simetría y dispersión de los datos.

Separamos las variables según su tipo, entre numéricas y categóricas:

In [ ]:
# Separar las variables por tipo
catg_var = df.select_dtypes(include=['object']).columns
numbr_var = df.select_dtypes(include=np.number).columns.tolist()

# Mostrar los resultados
print("Variables categóricas:")
print(catg_var)
print("Variables numéricas:")
print(numbr_var)
Variables categóricas:
Index(['hora'], dtype='object')
Variables numéricas:
['vento_direcao', 'vento_velocidade', 'umid_max_ant', 'umid_min_ant', 'temp_max_ant', 'temp_min_ant', 'umid_horaria', 'pressao_horaria', 'precipitacao', 'vento_rajada_max', 'pressao_max_ant', 'pressao_min_ant']
In [ ]:
# Seleccionar los colores a utilizar
colors = ['mediumorchid', 'darkorchid', 'darkslateblue',
          'mediumslateblue', 'indigo', 'darkmagenta',
          'orchid', 'plum']

for i, col in enumerate(numbr_var):
    # Mostrar estadísticas de simetría y distribución
    print(f'Column: {col}')
    print(f'Skew: {round(df[col].skew(), 2)}')
    print(f'Kurtosis: {round(df[col].kurtosis(), 2)}')

    # Crear gráficos
    plt.figure(figsize=(12, 4))

    # Histograma
    plt.subplot(1, 2, 1)
    df[col].hist(color=colors[i % len(colors)], alpha=0.7, edgecolor='black')
    plt.title(f'{col} - Historama')
    plt.grid(False)

    # Diagrama de Caja y Bigotes
    plt.subplot(1, 2, 2)
    sns.boxplot(x=df[col], color=colors[i % len(colors)])
    plt.title(f'{col} - Diagrama de Caja y Bigotes')
    plt.xlabel('')

    plt.tight_layout()
    plt.show()
Column: vento_direcao
Skew: -0.86
Kurtosis: -0.82
No description has been provided for this image
Column: vento_velocidade
Skew: 0.37
Kurtosis: -0.11
No description has been provided for this image
Column: umid_max_ant
Skew: -0.48
Kurtosis: -0.78
No description has been provided for this image
Column: umid_min_ant
Skew: -0.23
Kurtosis: -0.96
No description has been provided for this image
Column: temp_max_ant
Skew: 0.26
Kurtosis: -0.42
No description has been provided for this image
Column: temp_min_ant
Skew: 0.24
Kurtosis: -0.12
No description has been provided for this image
Column: umid_horaria
Skew: -0.35
Kurtosis: -0.89
No description has been provided for this image
Column: pressao_horaria
Skew: 6.18
Kurtosis: 179.64
No description has been provided for this image
Column: precipitacao
Skew: 17.65
Kurtosis: 476.4
No description has been provided for this image
Column: vento_rajada_max
Skew: 0.41
Kurtosis: 0.11
No description has been provided for this image
Column: pressao_max_ant
Skew: 1.02
Kurtosis: 5.82
No description has been provided for this image
Column: pressao_min_ant
Skew: 0.33
Kurtosis: 4.91
No description has been provided for this image

De acuerdo con los gráficos obtenidos, podemos interpretar algunos patrones entre las variables.

  • Humedad (umid_max_ant, umid_min_ant, umid_horaria): Las variables relacionadas con la humedad muestran distribuciones relativamente normales concentradas entre un 60% y 80%, lo que sugiere condiciones de humedad estables y tipicas para la región.
  • Temperatura (temp_max_ant, temp_min_ant): Las variables de temperatura mínima y máxima presentan una distribución simétrica con valores skew de 0.24 y 0.26, respectivamente. Las temperaturas mínimas estan concentradas en 18-20°C,mientras que las temperaturas máximas estan concentradas en valores entre 20-22°C. Esto sugiere que es una región con clima templado con poca variación térmica.
  • Viento (vento_direcao, vento_velocidade, vento_rajada_max): La dirección del viento muestra una fuerte concentración en valores positivos (0.5-1.0), lo que sugiere vientos predominanted desde una dirección específica. La velocidad del viento presenta una distribución asimétrica hacia la derecha, con la mayoría de valores bajos (2-4 unidades) y pocos eventos de vientos fuertes. Adicionalemnte, las ráfagas máximas siguen un patrón similar.
  • Precipitación: Tiene la distribución más asimétrica de todas las variables, con la mayoría de observaciones en cero (días secos) y muy pocos eventos de lluvia significativa. Sin embargo, esto es típico en datos asociados a precipitación.
  • Presión Atmosférica: Las tres variables de presión muestran distribuciones muy concentradas. La presión horaria está fuertemente concentrada alrededore de 880-900 hPa. Por su parte, la presión mínima y máxima tienen distribuciones similares pero ligeramente más dispersas.

En general los datos sugieren un clima caracterizado por condiciones generalmente secas con precipitaciones ocasionales. La temperatura es templada y estable.Adicionalmente, se caracteriza por una humedad moderada a alta, con vientos predominantes de una dirección específica y presión atmosférica estable. Más aún los diagramas de caja y bigotes confirman los patrones identificados por los histogramas y ayudan a identificar la presencia de valores atípicos, especialmente en las variables relacionadas con precipitación y viento, lo que es normal en datos meteorológicos donde los eventos extremos son poco frecuentes pero significativos.

Utilizando diagrama de barras representamos la variable hora para identificar la distribución de los datos a lo largo del día y ver que no haya sesgos temporales en la recolección de datos.

In [ ]:
# Tomamos la variable hora como categórica
df['hora'] = df['hora'].astype(str)

# Gráfico de barras
plt.figure(figsize=(12, 6))
sns.countplot(x="hora", data=df, hue="hora", palette='magma', legend=False)
plt.title('Distribución de las Observaciones por Hora del Día', fontsize=14)
plt.xlabel('Hora del día', fontsize=12)
plt.ylabel('Número de observaciones', fontsize=12)
plt.xticks(rotation=45)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
No description has been provided for this image

El gráfico muestra la completitud del data set. Podemos observar que cada hora tiene aproximadamente entre 3,500 a 3,600 observaciones. La distribución uniforme a lo largo del día nos indica que no hay horas perdidas o sub-repreentadas, lo que facilitará análisis posteriores que dependan del tiempo. Esto es consistente con los sistemas de monitoreo automático, donde hay mediciones probablemente cada hora de forma continua.

f. Análisis bivariado¶

Por medio del análisis bivariado podemos entender como se relacionan las diferentes variables de nuestro dataset entre sí. Para esto utilizaremos scatterplots que nos permitan ver la relación de la variable dependiente vento_velocidade y las variables independientes.

In [ ]:
def scatter_regplot(data, strx, stry):
    sns.set(font_scale=1.2)
    fig, ax = plt.subplots(1, 2, figsize=(14, 6), sharey=True)

    # Scatterplot sin línea de tendencia
    sns.scatterplot(data=data, x=strx, y=stry, ax=ax[0], alpha=0.3, color='purple')
    ax[0].set_title(f'Scatterplot: {stry} vs {strx}')

    # Scatterplot con línea de regresión
    sns.regplot(data=data, x=strx, y=stry, ax=ax[1], scatter_kws={'alpha': 0.3}, line_kws={"color": "red"})
    ax[1].set_title(f'Regplot: {stry} vs {strx}')

    fig.suptitle(f'Relación entre {strx} y {stry}', fontsize=16)
    plt.tight_layout()
    plt.show()

# Variable dependiente
target = 'vento_velocidade'

# Variables independientes (todas las demás numéricas)
independent_vars = [col for col in numbr_var if col != target]

# Graficar
for col in independent_vars:
    scatter_regplot(df, col, target)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Al analizar los gráficos de dispersión con líneas de regresión, podemos observar lo sigueinte patrones claves de correlación entre las varibles independientes y la variable dependiente.

Las correlaciones más notables se observan entre la velocidad del viento y las variables de presión atmosférica. Tanto la presión horaria como las presiones máximas y mínima anteriores muestran relaciones positivas fuerte con la velocidad del viento, esto se refleja en las pendientes de las líneas de regresión. Esta relación es clave en la meteorología, ya que los sistemas de alta presión tienden a generar vientos más intensos devido a los gradientes de presión creados en la atmosfera.

La relación entre velocidad del viento y rafagas máximas presentan la correlación más fuerte, como se puede observar por la relación lineal. Esto se explica en la práctica debido a que las rafagas son picos de velocidad con respecto a base.

Las variables de temperatura muestran correlaciones positivas con la velocidad del viento. La temperatura máxima y mínima anterior tiende a asociarse con vientos más fuertes o débiles.

En contraste, las variables de humedad presentan correlaciones negativas débiles con la velocidad del viento. La relación inversa sugiere que en condiciones humedas hay viento más débiles. Esto es coherente con el hecho de que en condiciones más secas se generar vientod más intensos debido a la menor estabilidad atmosférica.

Más aún, otra relación débil se observa con la dirección del viento y la precipitación. La dirección del viento muestra una correlación prácticamente nula con su velocidad, lo que indica que la intensidad del viento es independiente de su direeción en esta región. Esto sugiere que los vientos pueden ser igualmente fuertes viniendo de cualquier dirección, lo que podría estár relacionado con la topografía local o los patrones climáticos regionales. Más aún, la precipitación también muestra una relación débil con con la velocidad del viento, con valores cercanos a cero, refeljando la naturaleza esporádica de los eventos de lluvia.

Las variables de presión atmosférica y temperatura emergen como los predictores más importantes para la velocidad del viento, mientras que la humedad y precipitación muestran menor capacidad predictiva.

Por último realizamos una matriz de correlación para analizar la correlación, positiva o negativa, entre las variables.

In [ ]:
# Correlación de Pearson
plt.figure(figsize=(12, 8))
sns.heatmap(df[numbr_var].corr(), annot=True, fmt=".2f", cmap="RdPu", square=True)
plt.title("Matriz de Correlación - Pearson")
plt.show()
No description has been provided for this image

1. Correlaciones más relevantes para vento_velocidade

  • vento_rajada_max: Correlación alta positiva (0.87). Esto indica que a mayor velocidad de ráfagas, mayor velocidad del viento.
  • vento_direcao: Correlación moderada positiva (0.26) La dirrección del viento influye, aunque no tan fuertemente.
  • pressao_max_ant: Correlación baja a moderada (0.24).
  • pressao_min_ant: Similar la presión máxima anterior (0.23)
  • temp_max_ant: Ligeramente positiva (0.25). Más calor podría estar asociado con mayor velocidad del viento.
  • temp_min_ant: Similar a la temperatura mínima anterior.

La variable predictora más importante es claramente vento_rajada_max. Las variables de temperatura y presión podrían ser útiles en un modelo multivariado, pero con menor peso.

2. Correlaciones fuertes entre variables predictoras (multicolinealidad)

  • umid_max_ant y umid_min_ant: Correlación de 0.98. Casi idéntica.
  • umid_max_ant y umid_horaria: Correlación de 0.98. Igual que la anterior.
  • pressao_max_ant y pressao_min_ant: Correlación de 0.96. Existe multicolinealidad entre estas dos variables.
  • pressao_horaria y pressao_max_ant: Correlación de 0.88.
  • pressao_horaria y pressao_min_ant: Correlación de 0.89, similar a la anterior.

3. Variables con correlaciones muy bajas

precipitacao (0.01), umid_max_ant (-0.28) y umid_horaria(-0.30) podrían no aportar valor predictivo directos o incluso tener efectos complejos sobre el modelo.

Dataset: Detección de Fraude¶

A continuación procedemos a realizar el EDA del dataset para la Detección de Fraude.

In [ ]:
# Cargar los datos de entrenamiento y prueba
train_transaction = pd.read_csv('train_transaction.csv')
train_identity = pd.read_csv('train_identity.csv')
test_transaction = pd.read_csv('test_transaction.csv')
test_identity = pd.read_csv('test_identity.csv')

# Ver dimensiones
print("Dimensiones de train_transaction:", train_transaction.shape)
print("Dimensiones de train_identity:", train_identity.shape)
print("Dimensiones de test_transaction:", test_transaction.shape)
print("Dimensiones de test_identity:", test_identity.shape)
Dimensiones de train_transaction: (52143, 394)
Dimensiones de train_identity: (144233, 41)
Dimensiones de test_transaction: (53549, 393)
Dimensiones de test_identity: (141907, 41)

train_transaction y test_transaction, contienen información sobre transacciones realizadas (por ejemplo, importe, tipo de tarjeta, email, dirección IP, etc.). Por su parte train_identity y test_identity, contienen información complementaria (de identidad del usuario, dispositivo, ubicación, etc.) para algunas de las transacciones.

train_transaction contiene 92.059 transacciones en el set de entrenamiento y contiene 394 variables. train_identity tiene 144,233 registros de individuos. Los conjuntos de testeo tienen una estructura similar al de entrenamiento, con test_transaction teniendo una columna menos que el conjunto de entrenamiento debido a que no se incluye la variable isFraud.

a. Unir los datasets¶

In [ ]:
# Unir transacciones con función identidad
train_df = pd.merge(train_transaction, train_identity, on='TransactionID', how='left')
test_df = pd.merge(test_transaction, test_identity, on='TransactionID', how='left')

# Verificar el tamaño y primeras filas
print("Shape of train_df:", train_df.shape)
print("Shape of test_df:", test_df.shape)
train_df.head()
Shape of train_df: (52143, 434)
Shape of test_df: (53549, 433)
Out[ ]:
TransactionID isFraud TransactionDT TransactionAmt ProductCD card1 card2 card3 card4 card5 ... id_31 id_32 id_33 id_34 id_35 id_36 id_37 id_38 DeviceType DeviceInfo
0 2987000 0 86400 68.5 W 13926 NaN 150.0 discover 142.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 2987001 0 86401 29.0 W 2755 404.0 150.0 mastercard 102.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 2987002 0 86469 59.0 W 4663 490.0 150.0 visa 166.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 2987003 0 86499 50.0 W 18132 567.0 150.0 mastercard 117.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4 2987004 0 86506 50.0 H 4497 514.0 150.0 mastercard 102.0 ... samsung browser 6.2 32.0 2220x1080 match_status:2 T F T T mobile SAMSUNG SM-G892A Build/NRD90M

5 rows × 434 columns

Luego de unir los conjuntos de datos, tenemos un conjunto de entrenamiento train_df con 92.059 filas y 434 columnas, y un conjunto de prueba test_df con 98.874 filas y 433 columnas.

La union de los datasets se realizó utilizando merge left, se conservaron todas las transacciones originales y se añadieron las columnas de identidad en las coincidencias con TransactionID en los datasets de identity.

A primera vista podemos notar que las columnas de identidad como id_31, id_32, DiviceType, DeviceInfo, etc. contienen valores faltantes o NaN. Esto indica que no todas las transacciones poseen información de identidad asociada. Esto se corregirá más adelante.

El dataset contiene las sigueintes variables:

  • TransactionDT: Intervalo de tiempo a partir de una fecha y hora de referencia.
  • TransactionAMT: Importe del pago de la transacción en USD
  • ProductCD: Código de producto, el producto de cada transacción card1 - card6 : Información de la tarjeta de pago, como tipo de tarjeta, categoría de tarjeta, banco emisor, país, etc.
  • addr : Dirección
  • dist: Distancia
  • P_ and (R_) emaildomain: Dominio de correo electrónico del comprador y del destinatario
  • C1-C14 : Recuento, cuántas direcciones se encuentran asociadas a la tarjeta de pago, etc. El significado real está codificado.
  • D1-D15 : Intervalo de tiempo, como los días transcurridos entre la transacción anterior, etc.
  • M1-M9 : Coinciden, como los nombres en la tarjeta y la dirección, etc.
  • Vxxx : Vesta ofrece una gran variedad de funciones, como la clasificación, el recuento y otras relaciones entre entidades.
  • DeviceType: Codificada. Información de identidad o conexión de red (IP, ISP, Proxy, etc) o firma digital
  • DeviceInfo: Codificada. Información de identidad o conexión de red (IP, ISP, Proxy, etc) o firma digital
  • id_12 - id_38: Codificada. Información de identidad o conexión de red (IP, ISP, Proxy, etc) o firma digital
In [ ]:
# Tipos de variables
cat_cols = train_df.select_dtypes(include=['object']).columns
num_cols = train_df.select_dtypes(include=np.number).columns

print("Variables categóricas:", len(cat_cols))
print("Variables numéricas:", len(num_cols))
Variables categóricas: 31
Variables numéricas: 403

b. Valores faltantes y tipos de variables¶

In [ ]:
# Conteo y porcentaje de valores faltantes
missing_count = train_df.isnull().sum()
missing_pct = (missing_count / len(train_df)) * 100
missing_data = pd.DataFrame({'Missing Count': missing_count, 'Missing %': missing_pct})
missing_data = missing_data[missing_data['Missing Count'] > 0].sort_values(by='Missing %', ascending=False)

print("Variables con datos faltantes:")
display(missing_data.head(10))
Variables con datos faltantes:
Missing Count Missing %
id_24 51552 98.866578
id_25 51513 98.791784
id_21 51507 98.780277
id_08 51504 98.774524
id_07 51504 98.774524
id_23 51503 98.772606
id_26 51503 98.772606
id_27 51503 98.772606
id_22 51503 98.772606
D7 49659 95.236177

Al analizar la proporción de valores faltantes en el conjunto de entrenamiento notamos que hay ciertas variables que presentan un nivel elevado de datos faltantes, superior al 98%. Principalmente son variables asociadas a la identidad. Adicionalmente, dentro de las variables temporales (D), la variable D7 también presenta el 94.03% de valores faltantes. Esto implica que estas variables contienen muy poca información útil en términos de cobertura, ya que están presentes en menos del 2% de las transacciones.

In [ ]:
# Distribución de la variable objetivo
sns.countplot(x='isFraud', data=train_df)
plt.title("Distribución de Fraude (0 = No fraude, 1 = Fraude)")
plt.xlabel("isFraud")
plt.ylabel("Cantidad")
plt.show()

# Mostrar Porcentajes
fraud_rate = train_df['isFraud'].value_counts(normalize=True) * 100
print("Distribución porcentual:\n", fraud_rate)
No description has been provided for this image
Distribución porcentual:
 isFraud
0    97.301651
1     2.698349
Name: proportion, dtype: float64

La gráfica muestra la distribución de fraude en el con junto de entrenamiento dónde el eje x representa la clasificación binaria donde 0 equivale a No fraude y 1 equivale a fraude, mientras que el eje y muestra el número de casos en cada categoría.

Podemos notar que hay un desequilibrio extremo en los datos. Los casos sin fraude son aproximadamente 90,000 casos, representando la gran mayoría del dataset. Por el contrario los casos con fraude son aproximadamente 2,000 a 3,000 casos, lo cuál es una proporción pequeña del total de los datos.

c. Análisis descriptivo¶

A continuación se presenta el análisis descriptivo de algunas de las variables descriptivas.

In [ ]:
# Comparar transacciones fraudulentas vs no fraudulentas por valor
plt.figure(figsize=(10, 5))
sns.histplot(data=train_df, x='TransactionAmt', hue='isFraud', bins=100, log_scale=True, element="step", stat="density", common_norm=False)
plt.title('Distribución de TransactionAmt por isFraud (log scale)')
plt.show()

# Agregar variables númericas adicionales
num_vars_to_plot = ['D1', 'C1']

for col in num_vars_to_plot:
    plt.figure(figsize=(10, 5))
    sns.kdeplot(data=train_df, x=col, hue='isFraud', fill=True, common_norm=False)
    plt.title(f'Distribución de {col} por isFraud')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Al comparar transacciones fraudulentas vs no fraudulentas por valor podemos concluir que:

  • Montos de transacción (Transacción Amt): Las transacciones legítimas se concentran fuertemente en montos bajos (menos de 100 USD). Por el contrario las transacciones fraudulentas muestran una distribución más amplia, extendiéndose hacia montos más altos. Hay una presencia significativa de fraude en el rango de 100-1000 USD, donde las transacciones legítimas son menos frecuentes. Más aún los montos muy altos (más de 1000 USD) parecen tener una proporción mayor de fraude relativo al volumen total.

Más aún al analizar el comportamiento de las transacciones fraudulentas en algunas de las variables numéricas D1 (variable temporal) y C1 (Conteos/frecuencias) podemos notar que:

  • D1: Las distribuciones son muy similares entre fraude y no fraude. Ambas se concentran cerca de 0 con colas largas hacia la derecha. Los valores altos de D1 (mayores a 400) prodrían ser más sospechos.
  • C1: Las transacciones legítimas se concentran en valores bajos de C1, más cercanos a cero. Las transacciones fraudulentas muestran una distribución más dispersa en C1. Hay una presencia clara de fraude en los valores medios y altos de C1 (mayores a 1000), los cuáles parecen ser altamente sospechosos.

De los gráficos anteriores podemos concluir que el fraude tiende a ocurrir en los "extremos" o "colas" de las distribuciones, es decir, montos más altos, valores temporales más largos, y conteos más elevados. Esto sugiere que las transacciones "atípicas" o fuera de los patrones normales son más propensas al fraude.

In [ ]:
# Visualización de variables categóricas clave
cat_vars_to_plot = ['ProductCD', 'card4', 'card6', 'DeviceType']

for col in cat_vars_to_plot:
    plt.figure(figsize=(8, 4))
    sns.countplot(data=train_df, x=col, order=train_df[col].value_counts().index, hue='isFraud')
    plt.title(f'Distribución de {col} por isFraud')
    plt.xticks(rotation=45)
    plt.tight_layout()
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Si analizamos algunas de las variables categoricas claves con isFraud, podemos concluir que:

  • ProductCD está dominado por la categoría 4 (52k transacciones) con muy poco fraude visible, mientras que las categorías 1, 2, 0 y 3 tienen volúmenes menores pero podrían tener tasas de fraude proporcionalmente más altas.
  • Card4 muestra un patrón similar con la categoría 4 siendo la de mayor prevalencia (58k), seguida por la categoría 2 (27k), mientras que las categorías 0, 1 y 3 presentan un menor número.
  • Card6 presenta solo dos categorías principales (2 y 1), con la categoría 2 siendo predominante (58k vs 30k).
  • DeviceType muestra que la categoría 1 (probablemente desktop) tiene la mayor cantidad de transacciones no fraudulentas con 55k transacciones, seguida por 0 (24k, posiblemente mobile) y 2 (10k), con el fraude aparentemente concentrado en estas categorías principales pero siendo difícil de distinguir visualmente debido a la cantidad de transacciones.
In [ ]:
# Matriz de correlación con isFraud
# Filtrar columnas numéricas que existen en el dataset antes de elminiar las columnas
num_cols_after_drop = train_df.select_dtypes(include=np.number).columns.tolist()
num_vars_filtered = [col for col in num_cols_after_drop if train_df[col].isnull().mean() < 0.9]

# Creamos un sub-dataframe con las variables filtradas
corr_df = train_df[num_vars_filtered].copy()
corr_df['isFraud'] = train_df['isFraud']

# Correlación
correlation_matrix = corr_df.corr(numeric_only=True)
isFraud_corr = correlation_matrix['isFraud'].sort_values(key=abs, ascending=False)[1:20]

# Gráfico de las variables más correlacionadas con isFraud
plt.figure(figsize=(8, 10))
sns.barplot(x=isFraud_corr.values, y=isFraud_corr.index)
plt.title("Top 20 variables más correlacionadas con isFraud")
plt.show()
No description has been provided for this image

Analizando las correlaciones con fraude, V201, V189, V45, V200 y V257 emergen como las variables más predictivas (correlaciones de 0.20-0.25), lo cual es excelente en detección de fraude donde estas magnitudes son muy significativas. Predominan las variables de la serie "V" o Vesta que representan funciones, como la clasificación, el recuento y otras relaciones entre entidades. card3 es la única variable categórica original entre las top 20. Estas correlaciones, aunque parecen ser moderadas, representan señales muy fuertes para fraude.

Identidad: Variables Numéricas¶
In [ ]:
charts = {}
for i in ['id_12', 'id_15', 'id_16', 'id_28', 'id_29', 'id_30', 'id_31', 'id_32', 'id_33', 'id_34', 'id_35', 'id_36', 'id_37', 'id_38']:
    feature_count = train_df[i].value_counts(dropna=False).reset_index()
    feature_count.columns = [i, 'count']
    chart = alt.Chart(feature_count).mark_bar().encode(
                y=alt.Y(f"{i}:N", axis=alt.Axis(title=i)),
                x=alt.X('count:Q', axis=alt.Axis(title='Count')),
                tooltip=[i, 'count']
            ).properties(title=f"Counts of {i}", width=400)
    charts[i] = chart

chart_to_display = (charts['id_12'] | charts['id_15'] | charts['id_16']) & (charts['id_28'] | charts['id_29'] | charts['id_32']) & (charts['id_34'] | charts['id_35'] | charts['id_36']) & (charts['id_37'] | charts['id_38'])

chart_to_display.display()

image.png

Este conjunto de gráficos muestra las distribuciones de variables categóricas (id_12 a id_38), lo que sugiere patrones consistentes de alta proporción de valores nulos y distribuciones extremadamente desbalanceadas. La mayoría de las variables presentan categorías binarias como "Found/NotFound" o "New/Unknown", donde los valores nulos dominan significativamente, seguidos por una categoría principal y una minoritaria con muy pocas observaciones. Este patrón es típico en datos de fraude donde muchas características representan metadatos opcionales sobre dispositivos, ubicaciones o historial de usuarios que no siempre están disponibles. El desequilibrio extremo en estas distribuciones presenta tanto desafíos (manejo de valores faltantes, clases desbalanceadas) como oportunidades (los valores minoritarios podrían ser indicadores fuertes de fraude).

In [ ]:
charts = {}
for i in ['id_30', 'id_31', 'id_33', 'DeviceType', 'DeviceInfo']:
    feature_count = train_df[i].value_counts(dropna=False)[:40].reset_index()
    feature_count.columns = [i, 'count']
    chart = alt.Chart(feature_count).mark_bar().encode(
                x=alt.X(f"{i}:N", axis=alt.Axis(title=i)),
                y=alt.Y('count:Q', axis=alt.Axis(title='Count')),
                tooltip=[i, 'count']
            ).properties(title=f"Counts of {i}", width=800)
    charts[i] = chart

(charts['id_30'] & charts['id_31'] & charts['id_33'] & charts['DeviceType'] & charts['DeviceInfo']).display()

image.png

Los gráficos muestran distribuciones de variables categóricas relacionadas con navegadores, sistemas operativos y dispositivos en el dataset de fraude, evidenciando patrones extremadamente concentrados donde los valores nulos dominan en todas las variables (id_30, id_31, id_33). Las variables id_30 e id_31 presentan múltiples categorías de navegadores y sistemas operativos respectivamente, pero con frecuencias muy bajas comparadas con los valores faltantes, mientras que id_33 muestra una distribución aún más extrema con prácticamente todos los valores siendo nulos.

En contraste, DeviceType exhibe una distribución más balanceada entre tres categorías principales (con "null" siendo la más frecuente, seguido por otras dos categorías de dispositivos), y DeviceInfo presenta una distribución altamente concentrada en valores nulos con algunas categorías específicas de dispositivos apareciendo en pequeñas proporciones.

Esta alta proporción de información faltante en metadatos de dispositivos y navegadores es típica en datasets de transacciones financieras donde esta información técnica no siempre está disponible o es capturada, sugiriendo que cuando estos valores están presentes podrían ser indicadores valiosos para la detección de fraude, especialmente considerando que los usuarios fraudulentos podrían usar configuraciones de dispositivos o navegadores menos comunes para evadir la detección de fraude.

Transacción: Variables Categóricas¶
In [ ]:
charts = {}
for i in ['ProductCD', 'card4', 'card6', 'M4', 'M1', 'M2', 'M3', 'M5', 'M6', 'M7', 'M8', 'M9']:
    feature_count = train_df[i].value_counts(dropna=False).reset_index()
    feature_count.columns = [i, 'count']
    chart = alt.Chart(feature_count).mark_bar().encode(
                y=alt.Y(f"{i}:N", axis=alt.Axis(title=i)),
                x=alt.X('count:Q', axis=alt.Axis(title='Count')),
                tooltip=[i, 'count']
            ).properties(title=f"Counts of {i}", width=400)
    charts[i] = chart

combined_charts = (charts['ProductCD'] | charts['card4']) & \
                  (charts['card6'] | charts['M4']) & \
                  (charts['M1'] | charts['M2']) & \
                  (charts['M3'] | charts['M5']) & \
                  (charts['M6'] | charts['M7']) & \
                  (charts['M8'] | charts['M9'])

combined_charts.display()

image.png

Estos gráficos muestran las distribuciones de variables relacionadas con productos financieros y características de coincidencia (match) en el dataset de fraude, mostrando patrones según el tipo de variable. Las variables de productos (ProductCD, card4, card6) muestaran distribuciones más balanceadas donde ProductCD muestra categorías como C, H, R, S y W con frecuencias variables, card4 está dominado por "mastercard" seguido de "visa", "discover" y "american express", y card6 presenta principalmente transacciones "debit" con algunas "credit" y categorías híbridas.

En contraste, las variables de coincidencia M1-M9 muestran el patrón ya familiar de alta número de valores nulos, pero con algunas diferencias notables: M4 tiene una distribución más equilibrada entre sus categorías no nulas (M0, M1, M2), mientras que variables como M3, M5, M6 y M7 muestran distribuciones binarias simples (principalmente T/F) con M6 siendo la excepción al mostrar mayor balance entre las categorías "T" y "F". Esta variabilidad en las distribuciones sugiere que las variables de productos podrían tener mayor poder predictivo debido a su mejor representación de categorías, mientras que las variables de match, aunque con muchos valores faltantes, podrían ser indicadores de fraude.

In [ ]:
charts = {}
for i in ['P_emaildomain', 'R_emaildomain', 'card1', 'card2', 'card3',  'card5', 'addr1', 'addr2']:
    feature_count = train_df[i].value_counts(dropna=False).reset_index()[:40]
    feature_count.columns = [i, 'count']
    chart = alt.Chart(feature_count).mark_bar().encode(
                x=alt.X(f"{i}:N", axis=alt.Axis(title=i)),
                y=alt.Y('count:Q', axis=alt.Axis(title='Count')),
                tooltip=[i, 'count']
            ).properties(title=f"Counts of {i}", width=600)
    charts[i] = chart

((charts['P_emaildomain'] | charts['R_emaildomain']) & (charts['card1'] | charts['card2']) & (charts['card3'] | charts['card5']) & (charts['addr1'] | charts['addr2'])).display()

image.png

Estos gráficos muestran las distribuciones de variables relacionadas con información de correo electrónico, tarjetas de crédito y direcciones en el dataset de fraude.

Las variables de dominio de email (P_emaildomain y R_emaildomain) están altamente concentradas con valores nulos, seguidos por algunos dominios específicos como "gmail.com" que aparecen con mayor frecuencia, mientras que la mayoría de otros dominios tienen representaciones muy bajas.

Las variables de tarjeta (card1, card2, card3, card5) muestran distribuciones más uniformes con múltiples categorías numéricas, donde card1 y card2 presentan distribuciones relativamente equilibradas a través de diferentes rangos numéricos, card3 está extremadamente concentrada en un valor específico, probablemente 150, y card5 muestra una distribución bimodal con dos valores dominantes.

Las variables de dirección (addr1 y addr2) exhiben el patrón familiar de alta concentración de valores nulos con addr1 mostrando cierta distribución entre diferentes códigos numéricos y addr2 principalmente con valores nulos y una sola categoría específica apareciendo con mayor frecuencia.

Esta variabilidad en las distribuciones sugiere que variables como card1, card2 y addr1 podrían tener mayor utilidad predictiva debido a su mejor distribución de valores, mientras que las altamente concentradas como card3 y las variables de email podrían requerir técnicas especiales de manejo para extraer su valor predictivo.

d. Eliminación e imputación de valores faltantes¶

In [ ]:
# Eliminar columnas con más de 95% de valores faltantes
cols_to_drop = train_df.columns[train_df.isnull().mean() > 0.95]
print("Columnas eliminadas:", len(cols_to_drop))
train_df.drop(columns=cols_to_drop, inplace=True)
Columnas eliminadas: 10
In [ ]:
# Imputar valores númericos con la mediana
num_cols_clean = train_df.select_dtypes(include=np.number).columns
imputer = SimpleImputer(strategy='median')
train_df[num_cols_clean] = imputer.fit_transform(train_df[num_cols_clean])
In [ ]:
# Imputar categóricas con 'missing' y codificar con LabelEncoder
cat_cols_clean = train_df.select_dtypes(include='object').columns
train_df[cat_cols_clean] = train_df[cat_cols_clean].fillna('missing')

le = LabelEncoder()
for col in cat_cols_clean:
    try:
        train_df[col] = le.fit_transform(train_df[col])
    except:
        train_df[col] = train_df[col].astype(str).apply(lambda x: hash(x) % 1000)
In [ ]:
# Separar X e y
X = train_df.drop(columns=['isFraud'])
y = train_df['isFraud']

# Identify columns after previous steps
cat_cols = X.select_dtypes(include='object').columns
num_cols = X.select_dtypes(include=np.number).columns

# Impute numerical columns
imputer_num = SimpleImputer(strategy='median')
X[num_cols] = imputer_num.fit_transform(X[num_cols])

# Impute and encode categorical columns
# Check if there are any categorical columns left before proceeding
if len(cat_cols) > 0:
    imputer_cat = SimpleImputer(strategy='constant', fill_value='missing')
    X[cat_cols] = imputer_cat.fit_transform(X[cat_cols])

    onehot_encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False) # Use sparse_output instead of sparse
    X_cat_encoded = onehot_encoder.fit_transform(X[cat_cols])

    # Get feature names for encoded categorical columns
    cat_feature_names = onehot_encoder.get_feature_names_out(cat_cols)

    # Combine numerical and encoded categorical features
    X_num_df = X[num_cols].reset_index(drop=True)
    X_cat_df = pd.DataFrame(X_cat_encoded, columns=cat_feature_names).reset_index(drop=True)

    X_df = pd.concat([X_num_df, X_cat_df], axis=1)
else:
    # If no categorical columns, just use the numerical dataframe
    X_df = X[num_cols].copy().reset_index(drop=True)
    print("No categorical columns found for encoding.")


print("✅ Data preprocessed and encoded.")
print("Shape of X_df:", X_df.shape)
display(X_df.head())
No categorical columns found for encoding.
✅ Data preprocessed and encoded.
Shape of X_df: (52143, 423)
TransactionID TransactionDT TransactionAmt ProductCD card1 card2 card3 card4 card5 card6 ... id_31 id_32 id_33 id_34 id_35 id_36 id_37 id_38 DeviceType DeviceInfo
0 2987000.0 86400.0 68.5 4.0 13926.0 370.0 150.0 1.0 142.0 1.0 ... 57.0 24.0 89.0 3.0 2.0 2.0 2.0 2.0 1.0 572.0
1 2987001.0 86401.0 29.0 4.0 2755.0 404.0 150.0 2.0 102.0 1.0 ... 57.0 24.0 89.0 3.0 2.0 2.0 2.0 2.0 1.0 572.0
2 2987002.0 86469.0 59.0 4.0 4663.0 490.0 150.0 4.0 166.0 2.0 ... 57.0 24.0 89.0 3.0 2.0 2.0 2.0 2.0 1.0 572.0
3 2987003.0 86499.0 50.0 4.0 18132.0 567.0 150.0 2.0 117.0 2.0 ... 57.0 24.0 89.0 3.0 2.0 2.0 2.0 2.0 1.0 572.0
4 2987004.0 86506.0 50.0 1.0 4497.0 514.0 150.0 2.0 102.0 1.0 ... 72.0 32.0 51.0 2.0 1.0 0.0 1.0 1.0 2.0 285.0

5 rows × 423 columns

Los resultados muestran que el dataset ha sido exitosamente preprocesado y codificado, transformándose de las variables categóricas originales que analizamos en los gráficos anteriores a un formato numérico listo para machine learning. El dataset final contiene 92,059 transacciones con 424 columnas, lo que implica una expansión significativa desde las variables originales debido al proceso de encoding categórico.

Las primeras filas del dataset transformado revelan que las variables categóricas que vimos en las gráficas anteriores han sido convertidas a valores numéricos, manteniendo las variables númericas originales. La dimensionalidad (424 columnas) refleja el gran número de variables categóricas en el dataset original, donde cada categoría única se convirtió en una columna binaria separada.

En la etapa de clasificación del modelo para predecir isFraud, no se utilizó OneHotEncoder debido a la gran cantidad de variables categóricas con alta cardinalidad, lo cual habría generado una expansión de columnas tras la codificación, afectando significativamente la eficiencia computacional y el rendimiento de los modelos, especialmente aquellos sensibles a la dimensionalidad como K-NN o SVM. Sin embargo, muchos de los algoritmos utilizados, como Random Forest y XGBoost, pueden trabajar directamente con variables codificadas sin necesidad de transformación one-hot. Por otro lado, aunque inicialmente se consideró aplicar la eliminación de multicolinealidad mediante el cálculo del Variance Inflation Factor (VIF), este procedimiento no se pudo ejecutar debido a las dimensiones del conjunto de datos y el alto costo computacional que implicaba. El proceso requería múltiples regresiones internas sobre más de 400 variables, lo que en la práctica llevó a tiempos de ejecución excesivamente largos o bloqueos del entorno. Por esta razón, se decidió continuar sin aplicar VIF.

Ejercicio No. 2. Modelos de Clasificiación¶

Antes de iniciar con el análisis de los modelos de clasificación definimos la validación cruzada estratificada. En la función cv se dividen los datos en 5 folds. Se entrena con 4 folds y se valida con 1, rotando. Más aún se mezclan los datos antes de dividirlos y se establece el random_state como 42 para garantizar que los datos sean reproducibles.

Adicionalmente creamos la lista tabla_resultados para almacenar los resultados de cada modelo evaluado y poder presentar un consolidado luego de correr todos los modelos.

In [ ]:
# Validación cruzada estratificada
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# Lista de resultados finales
tabla_resultados = []

Por otra parte definimos el conjunto de entrenamiento y prueba:

  • X_train, y_train: datos de entrenamiento (80%)
  • X_test, y_test: datos de prueba (20%)
In [ ]:
# División en train/test con estratificación
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

a. K-NN¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
# Pipeline
pipeline_knn = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', KNeighborsClassifier())
])

# Espacio de búsqueda bayesiana
search_space_knn = {
    'clf__n_neighbors': Integer(3, 25),
    'clf__p': Categorical([1, 2])
}

# BayesSearchCV
opt_knn = BayesSearchCV(
    estimator=pipeline_knn,
    search_spaces=search_space_knn,
    cv=cv,
    n_iter=10,
    scoring='roc_auc',
    n_jobs=-1,
    random_state=42,
    verbose=0
)

opt_knn.fit(X_train, y_train)
print("K-NN entrenado con los mejores hiperparámetros:")
print(opt_knn.best_params_)
K-NN entrenado con los mejores hiperparámetros:
OrderedDict([('clf__n_neighbors', 21), ('clf__p', 1)])

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = opt_knn.predict(X_test)
y_proba = opt_knn.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Clasificación:")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Clasificación:
              precision    recall  f1-score   support

         0.0       0.97      1.00      0.99      3050
         1.0       1.00      0.02      0.23        85

    accuracy                           0.98      3155
   macro avg       0.99      0.51      0.52      3155
weighted avg       0.97      0.97      0.96      3155

AUC: 0.8201485053037608

Por los resultados del accuracy el modelo muestra un buen rendimiento general con un resultado del 97% y un AUC de 0,82. Sin embargo, existe un problema significativo de desequilibrio de clases: si bien el modelo identifica perfectamente las transacciones no fraudulentas (clase 0: precision = 1,0, recall = 1,0), presenta dificultades para la detección de fraudes (clase 1: precision = 1,0, pero recall = 0,02). Esto significa que el modelo rara vez predice el fraude, pero cuando lo hace, es muy preciso.

Matriz de confusión

In [ ]:
# Matriz de confusión
knn_cm = confusion_matrix(y_test, y_pred)
f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(knn_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('K-NN - Matriz de Confusión')
plt.xlabel('Y predicho')
plt.ylabel('Y real')
plt.tight_layout()
plt.show()

image.png

La matriz de confusión confirma el problema de desequilibrio de clases con 3050 verdaderos negativos y solo 85 casos de fraude en total. El modelo identificó correctamente solo 2 de las 85 transacciones fraudulentas, pasando por alto 83 casos de fraude (alta tasa de falsos negativos). Este enfoque minimiza las falsas alarmas, pero no logra detectar la mayoría de los fraudes reales.

Curva ROC

In [ ]:
# Curva ROC
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"K-NN (AUC = {auc:.3f})", color='darkblue')
ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - K-NN", fontsize=16)
ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
ax.legend(loc="lower right")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

image.png

La curva ROC muestra una capacidad para discriminar entre clases relativamente buena con un AUC de 0,82, significativamente mejor que la del azar (0,5). La forma de la curva indica que el modelo puede distinguir entre clases razonablemente bien, pero la matriz de confusión revela que, en la práctica, el umbral de decisión es demasiado conservador y prioriza la precisión sobre la recuperación para la detección de fraude.

b. Logistic Regression¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
# Pipeline
pipeline_lr = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(class_weight='balanced', max_iter=1000, solver='liblinear'))
])

# Espacio de búsqueda bayesiana
search_space_lr = {
    'clf__C': Real(1e-3, 100, prior='log-uniform'),
    'clf__penalty': Categorical(['l1', 'l2']),
}

# BayesSearchCV
opt_lr = BayesSearchCV(
    estimator=pipeline_lr,
    search_spaces=search_space_lr,
    cv=cv,
    n_iter=10,
    scoring='roc_auc',
    n_jobs=-1,
    random_state=42,
    verbose=0
)

opt_lr.fit(X_train, y_train)
print("Regresión Logística entrenada con los mejores hiperparámetros:")
print(opt_lr.best_params_)

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = opt_lr.predict(X_test)
y_proba = opt_lr.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Clasificación:")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Clasificación:
              precision    recall  f1-score   support

         0.0       0.99      0.78      0.87      3500
         1.0       0.09      0.75      0.15        85

    accuracy                           0.77      3135
   macro avg       0.54      0.76      0.51      3135
weighted avg       0.97      0.77      0.85      3135

AUC: 0.8506576663452265

El modelo muestra un desempeño bueno con un accuracy del 77% y un AUC de 0.85. Hay una mejora notable en la detección de fraude en comparación con el modelo anterior (K-NN), mientras que el precision para la detección de fraude (clase 1) disminuye a 0.09, el recall aumenta a 0.75. Esto impica que el modelo ahora captura el 75% de las transacciones fraudulentas pero aún puede generar muchas falsas alarmas. El balance del F1-score refleja este intercambio entre precision y recall.

Matriz de confusión

In [ ]:
lr_cm = confusion_matrix(y_test, y_pred)
f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(lr_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('Logistic Regression - Matriz de Confusión')
plt.xlabel('Y predicho')
plt.ylabel('Y real')
plt.show()

image.png

La matriz de confusión revela una aproximación más agresiva para la detección de fraude. de los 85 casos totales de fraude el modelo identifica correctamente 64 (mucho mejor que los 2 que identifica el modelo K-NN), pero esto implica el costo de 685 falsos positivos. Esto representa un cambio en la estrategia de una más conservadora a una más liberal, priorizando la captura de fraude sobre la reducción de las falsas alarmas. EL modelo ahora captura más operaciones fraudulentas pero marca muchas transacciones legítimas como sospechosas.

Curva ROC

In [ ]:
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"LogReg (AUC = {auc:.3f})", color='royalblue')
ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - Regresión Logística", fontsize=14)
ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

image.png

La curva ROC muestra un desempeño discriminativo bueno con un AUC de 0.85. La pendiente inclinada inicial y el área balo la curva indican una fuerte habilidad para distinguir entre transacciones fraudulentas y legítimas. Esto sugiere que el modelo ha aprendido patrones en la data que pueden ayudar a separar adecuadamente ambas clases cuando el threshold de decisión está calibrado adecuadamente.

c. Bayesian Classification¶

Definición del Pipeline

In [ ]:
# Pipeline
pipeline_nb = Pipeline([
    ('scaler', StandardScaler()),  # opcional para GaussianNB
    ('clf', GaussianNB())
])

# Entrenamiento
pipeline_nb.fit(X_train, y_train)
Out[ ]:
Pipeline(steps=[('scaler', StandardScaler()), ('clf', GaussianNB())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('scaler', StandardScaler()), ('clf', GaussianNB())])
StandardScaler()
GaussianNB()

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = pipeline_nb.predict(X_test)
y_proba = pipeline_nb.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Clasificación:")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Clasificación:
              precision    recall  f1-score   support

         0.0       1.00      0.20      0.33      3050
         1.0       0.03      0.96      0.06        85

    accuracy                           0.22      3135
   macro avg       0.51      0.58      0.20      3135
weighted avg       0.97      0.22      0.33      3135

AUC: 0.6073095467695275

El modelo muestra un desempeño bastante bajo con accuracy de solamente el 22% y un AUC débil del 0.61. Mientras que alcanza un buen puntaje de recall de fraude, implando que logra identificar el 96% de las transacciones fraudulentas, el puntaje de precision es extemadamente bajo con 0.03. Esto indica que el modelo es supremamente agresivo, marcando casi todos los casos como fraude. Los promedios ponderados reflejan un impacto considerable en la tasa de falsos positivos en el desempeño general.

Matriz de confusión

In [ ]:
# Matriz de confusión
nb_cm = confusion_matrix(y_test, y_pred)
f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(nb_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('Naive Bayes - Matriz de Confusión')
plt.xlabel('Y predicho')
plt.ylabel('Y real')
plt.tight_layout()
plt.show()

image.png

La matriz de confusión revela una sobrepredicción del fraude. De los 85 casos actuales de fraude el modelo identifica 82, pero tambien marca de manera incorrecta 2,437 de los 3,050 casos de transacciones legítimas como fraudulentas. Esto representa una tasa de falsos positivos cercana al 80%, haciendo que el sistema sea practicamente obsoleto en el mundo real.

Curva ROC

In [ ]:
# Curva ROC
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"Naive Bayes (AUC = {auc:.3f})", color='lightcoral')
ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - Naive Bayes", fontsize=14)
ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

image.png

La curva ROC muestra una habilidad de discriminación baja con un AUC de 0.61, solo marginalmente mejor que el azar por 0.5. La forma de la curva indica la dificultad del modelo para realizar la separación entre clases, sugiriendo que el modelo se le dificulta aprender patrones en el dataset de detección de fraude. El supuesto de independencia del modelo Bayesiano no se mantiene bien para este problema particular de detección de fraude.

d. Decision Tree¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
# Pipeline
pipeline_dt = Pipeline([
    ('clf', DecisionTreeClassifier(class_weight='balanced', random_state=42))
])

# Espacio de búsqueda
search_space_dt = {
    'clf__max_depth': Integer(3, 20),
    'clf__min_samples_split': Integer(2, 20),
    'clf__min_samples_leaf': Integer(1, 10)
}

# BayesSearchCV
opt_dt = BayesSearchCV(
    estimator=pipeline_dt,
    search_spaces=search_space_dt,
    cv=cv,
    n_iter=10,
    scoring='roc_auc',
    n_jobs=-1,
    random_state=42,
    verbose=0
)

opt_dt.fit(X_train, y_train)
print("Decision Tree entrenado con los mejores hiperparámetros:")
print(opt_dt.best_params_)
Decision Tree entrenado con los mejores hiperparámetros:
OrderedDict([('clf__max_depth', 10), ('clf__min_samples_leaf', 8), ('clf__min_samples_split', 19)])

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = opt_dt.predict(X_test)
y_proba = opt_dt.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Clasificación::")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Clasificación::
              precision    recall  f1-score   support

         0.0       0.99      0.82      0.90      3050
         1.0       0.10      0.75      0.18        85

    accuracy                           0.82      3135
   macro avg       0.55      0.79      0.54      3135
weighted avg       0.97      0.82      0.88      3135

AUC: 0.8254811957569913

El modelo de árboles de decisiones demuestra un buen desempeño con un accuracy del 82% y un AUC de 0.83. Alcanza una buena aproximación a la detección de fraude con el 75% de recall (detecta 3 de cada 4 casos de fraude) y 10% de precision para la clase 1. A pesar the que el porcentaje de precision es bajo, es significativamente mejor que el modelo Native Bayes, y el recall es mucho más alto que el del modelo K-NN, representando un punto medio entre los extremos de los modelos anteriores.

Matriz de confusión

In [ ]:
# Matriz de confusión
dt_cm = confusion_matrix(y_test, y_pred)
f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(dt_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('Decision Tree - Matriz de Confusión')
plt.xlabel('Y predicho')
plt.ylabel('Y real')
plt.tight_layout()
plt.show()

image.png

La matriz muestra que el modelo de árbol de decisión encuentra un mejor balance que los modelos previos. De los 85 casos de fraude, identifica correctamente 64 (similar al modelo de regesión logística), pero genera un menor número de falsos positivos (548 vs. 685). Esto sugiere que un enfoque basado en árboles conlleva a decisiones que premiten distinguir mejor entre transacciones legitimas y fraudulentas, reduciendo las alerta inecesarias y manteniendo buenas tasas de detección de fraude.

Curva ROC

In [ ]:
# Curva ROC
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"Decision Tree (AUC = {auc:.3f})", color='orange')
ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - Decision Tree", fontsize=16)
ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

image.png

En esta curva de ROC, podemos ver una mejora en el desempeño del modelo con un AUC de 0.83, similar al desempeño del modelo de regresión logística. La pendiente inclinada inicial de la curva indica que el modelo logra aprender los patrones para la detección de fraude de la base de datos. La estructura del árbol permite reglas de decisión interpretables que capturan relaciones complejas y no lineales entre características, lo que lo hace eficaz y potencialmente más explicable para los investigadores de fraude en comparación con otros modelos.

e. Random Forest¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
# Pipeline
pipeline_rf = Pipeline([
    ('scaler', StandardScaler()),  # Aunque RF no lo necesita, lo incluimos para estructura uniforme
    ('clf', RandomForestClassifier(class_weight='balanced', random_state=42))
])

# Espacio de búsqueda bayesiana
search_space_rf = {
    'clf__n_estimators': Integer(100, 300),
    'clf__max_depth': Integer(5, 20),
    'clf__min_samples_split': Integer(2, 10),
}

# Validación cruzada estratificada
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# BayesSearchCV
opt_rf = BayesSearchCV(
    estimator=pipeline_rf,
    search_spaces=search_space_rf,
    n_iter=10,
    cv=cv,
    scoring='roc_auc',
    n_jobs=-1,
    random_state=42,
    verbose=0
)

opt_rf.fit(X_train, y_train)
print("Random Forest entrenado con los mejores hiperparámetros:")
print(opt_rf.best_params_)
Random Forest entrenado con los mejores hiperparámetros:
OrderedDict([('clf__max_depth', 19), ('clf__min_samples_split', 8), ('clf__n_estimators', 274)])

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = opt_rf.predict(X_test)
y_proba = opt_rf.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Classificación:")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Classificación:
              precision    recall  f1-score   support

         0.0       0.98      1.00      0.99      3050
         1.0       0.92      0.42      0.58        85

    accuracy                           0.98      3135
   macro avg       0.95      0.71      0.79      3135
weighted avg       0.98      0.98      0.98      3135

AUC: 0.927760846017357

En general, el modelo alcanza un buen desempeño con un puntaje de accuracy del 98% y un AUC de 0.93, entre los modelos realizados hasta el momento. Demuestra un buen puntaje de precision (92%) para la detección de fraude y manteniendo un buen recall (42%). Esto representa un buen balance para la detección de fraude y la minimización de falsas alarmas. El alto puntaje de precision implica que cuando el modelo marca una transacción como fraudulenta, es correcto el 92% del tiempo, lo que lo hace más confiable.

Matriz de confusión

In [ ]:
# Matriz de confusión
rf_cm = confusion_matrix(y_test, opt_rf.predict(X_test))
f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(rf_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('Random Forest - Matriz de Confusión')
plt.xlabel('Y predicho')
plt.ylabel('Y real')
plt.show()

image.png

La matriz de confusión tiene un enfoque más conservador pero más preciso. De los 85 casos de fraude, identifica correctamente 36 (el recall es más bajo que en otros modelos), pero genera únicamente 3 falsos positivos, muchos menos que los modelos que hemos visto hasta ahora. Esta tasa tan baja de falsos positivos (0.1%) implica que el sistema muy rara vez marcará transacciones legítimas como incorrectas.

Curva ROC

In [ ]:
# Curva ROC
fig, ax = plt.subplots(figsize=(8, 6))

roc_display = RocCurveDisplay.from_predictions(
    y_test, y_proba,
    ax=ax,
    name=f"Random Forest (AUC = {auc:.3f})",
    color='palevioletred'
)

ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - Random Forest", fontsize=14)
ax.set_xlabel("Tasa de Falsos Positivos (1 - Especificidad)", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos (Sensibilidad)", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)

plt.tight_layout()
plt.show()

image.png

La curva ROC demuestra una habilidad buena a la hora de descriminar cada uno de los casos con un AUC de 0.93. La pendiente inclinada sugiere que el modelo aprende los patrones en el conjunto de datos. El enfoque de conjunto de combinar múltiples árboles de decisión permite capturar interacciones de características complejas y evitar el sobreajuste, lo que da como resultado el rendimiento de detección de fraude más sólido y confiable en todas las métricas.

f. XGBoost¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
# Pipeline
pipeline_xgb = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', XGBClassifier(
        use_label_encoder=False,
        eval_metric='logloss',
        scale_pos_weight=(y_train == 0).sum() / (y_train == 1).sum(),  # manejo del desbalance
        random_state=42
    ))
])

# Espacio de búsqueda
search_space_xgb = {
    'clf__n_estimators': Integer(100, 300),
    'clf__max_depth': Integer(3, 10),
    'clf__learning_rate': Real(0.01, 0.3, prior='log-uniform'),
    'clf__subsample': Real(0.6, 1.0),
    'clf__colsample_bytree': Real(0.6, 1.0),
}

# BayesSearchCV
opt_xgb = BayesSearchCV(
    estimator=pipeline_xgb,
    search_spaces=search_space_xgb,
    cv=cv,
    n_iter=10,
    scoring='roc_auc',
    n_jobs=-1,
    random_state=42,
    verbose=0
)

opt_xgb.fit(X_train, y_train)
print("XGBoost entrenado con los mejores hiperparámetros:")
print(opt_xgb.best_params_)
/usr/local/lib/python3.11/dist-packages/xgboost/core.py:158: UserWarning: [00:08:19] WARNING: /workspace/src/learner.cc:740: 
Parameters: { "use_label_encoder" } are not used.

  warnings.warn(smsg, UserWarning)
XGBoost entrenado con los mejores hiperparámetros:
OrderedDict([('clf__colsample_bytree', 0.7640415835413256), ('clf__learning_rate', 0.11883357504897696), ('clf__max_depth', 10), ('clf__n_estimators', 163), ('clf__subsample', 0.8680591793075738)])

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = opt_xgb.predict(X_test)
y_proba = opt_xgb.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Clasificación:")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Clasificación:
              precision    recall  f1-score   support

         0.0       0.99      1.00      0.99      3050
         1.0       0.87      0.65      0.74        85

    accuracy                           0.99      3135
   macro avg       0.93      0.82      0.87      3135
weighted avg       0.99      0.99      0.99      3135

AUC: 0.9616162005785921

El modelo XGBoost tiene el mejor desempeño de todos los modelos con un accuracy del 99% y un AUC de 0.96. Alcanza un precision de 87% para la detección de fraude y un recall de 65%, representando un balance óptimo entre presición y la capacidad de detección de fraude. El modelo identifica correctamente cerca de dos tercios de las transacciones fraudulentas mientras que mantiene una alta fiabilidad al momento de detectar actividad sospechosa, haciendolo altamente práctico para su uso en el mundo real.

Matriz de confusión

In [ ]:
# Matriz de confusión
xgb_cm = confusion_matrix(y_test, y_pred)

f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(xgb_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('XGBoost - Matriz de Confusión', fontsize=14)
plt.xlabel('Y predicho', fontsize=12)
plt.ylabel('Y real', fontsize=12)
plt.tight_layout()
plt.show()

image.png

La matriz de confusión para el modelo XGBoost alcanza el mejor balance entre los modelos testeados. De los 85 casos de fraude identifica correctamente 55, y solo genera 8 casos de falsos positivos. Esto representa un buen trade-off identificando 65% de los casos de fraude y manteniendo una tasa baja de falsas alarmas (0.26%).

Curva ROC

In [ ]:
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"XGBoost (AUC = {auc:.3f})", color='teal')
ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - XGBoost", fontsize=14)
ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

image.png

La curva ROC demuestra un buen desempeño con un AUC de 0.96, el más alto entre los diferentes modelos. La pendeidnte pronunciada y la cobertura del área bajo la curva indica que el modelo aprendió los patrones más sofisticados del conjunto de datos para la detección de fraude. El enfoque de gradiente perimite al modelo mejorar iterativamente sus predicciones aprendiendo de los errores previos, lo que resulta en un sistema de detección de fraude más robusto y fiable, que separa eficazzmente las transacciones fraudulentas de las legítimas en todos los umbrales de probabilidad.

g. Maquina de soporte vectorial - SVM***¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
## Pipeline
#pipeline_svm = Pipeline([
#    ('scaler', StandardScaler()),
#    ('clf', SVC(probability=True, class_weight='balanced'))
#])

## Espacio de búsqueda bayesiana
#search_space_svm = {
#    'clf__C': Real(1e-2, 100, prior='log-uniform'),
#    'clf__kernel': Categorical(['linear', 'rbf']),
#    'clf__gamma': Real(1e-4, 1.0, prior='log-uniform')
#}

## BayesSearchCV
#opt_svm = BayesSearchCV(
#    estimator=pipeline_svm,
#    search_spaces=search_space_svm,
#    cv=cv,
#    n_iter=10,
#    scoring='roc_auc',
#    n_jobs=-1,
#    random_state=42,
#    verbose=0
#)

#opt_svm.fit(X_train, y_train)
#print("SVM entrenado con los mejores hiperparámetros:")
#print(opt_svm.best_params_)

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
# y_pred = opt_svm.predict(X_test)
# y_proba = opt_svm.predict_proba(X_test)[:, 1]

# Métricas
# precision = precision_score(y_test, y_pred)
# recall = recall_score(y_test, y_pred)
# f1 = f1_score(y_test, y_pred)
# auc = roc_auc_score(y_test, y_proba)

# print("Reporte de Clasificación:")
# print(classification_report(y_test, y_pred))
# print("AUC:", auc)

Matriz de confusión

In [ ]:
# Matriz de confusión
# svm_cm = confusion_matrix(y_test, y_pred)
# f, ax = plt.subplots(figsize=(6, 4))
# sns.heatmap(svm_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
# plt.title('SVM - Matriz de Confusión')
# plt.xlabel('Y predicho')
# plt.ylabel('Y real')
# plt.tight_layout()
# plt.show()

Curva ROC

In [ ]:
# Curva ROC
# fig, ax = plt.subplots(figsize=(8, 6))
# RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"SVM (AUC = {auc:.3f})", color='purple')
# ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
# ax.set_title("Curva ROC - SVM", fontsize=14)
# ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
# ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
# ax.legend(loc="lower right", fontsize=10)
# ax.grid(alpha=0.3)
# plt.tight_layout()
# plt.show()

h. Multi-Layer Perceptron - MLP¶

Definición del Pipeline y del espacio de busqueda bayesiana

In [ ]:
# Pipeline
pipeline_mlp = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', MLPClassifier(max_iter=300, random_state=42))
])

# Espacio de búsqueda bayesiana
search_space_mlp = {
    'clf__hidden_layer_sizes': Integer(50, 200),
    'clf__alpha': Real(1e-5, 1e-2, prior='log-uniform'),
    'clf__learning_rate_init': Real(1e-4, 1e-1, prior='log-uniform')
}

# BayesSearchCV
opt_mlp = BayesSearchCV(
    estimator=pipeline_mlp,
    search_spaces=search_space_mlp,
    cv=cv,
    n_iter=10,
    scoring='roc_auc',
    n_jobs=-1,
    random_state=42,
    verbose=0
)

opt_mlp.fit(X_train, y_train)
print("MLP entrenado con los mejores hiperparámetros:")
print(opt_mlp.best_params_)
MLP entrenado con los mejores hiperparámetros:
OrderedDict([('clf__alpha', 0.00042678505501429927), ('clf__hidden_layer_sizes', 188), ('clf__learning_rate_init', 0.003086029766693725)])
/usr/local/lib/python3.11/dist-packages/sklearn/neural_network/_multilayer_perceptron.py:698: UserWarning: Training interrupted by user.
  warnings.warn("Training interrupted by user.")

Predicciones, cálculo de métricas y reporte de clasificacción

In [ ]:
# Predicciones
y_pred = opt_mlp.predict(X_test)
y_proba = opt_mlp.predict_proba(X_test)[:, 1]

# Métricas
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)

print("Reporte de Clasificación:")
print(classification_report(y_test, y_pred))
print("AUC:", auc)
Reporte de Clasificación:
              precision    recall  f1-score   support

         0.0       0.98      0.99      0.99      3050
         1.0       0.51      0.45      0.48        25

    accuracy                           0.97      3135
   macro avg       0.75      0.72      0.73      3135
weighted avg       0.97      0.97      0.97      3135

AUC: 0.8270163934426229

El modelo MLP muestra un desempeño bueno con un accuracy del 97% y un AUC de 0.83. Alcanza una capacidad para la detección de fraude moderada con un 51% de precision y 45% de recall para los casos de fraude. Esto representa un rendimiento intermedio, mejor que algunos modelos, pero no tan solido como los métodos de conjunto. El enforque de redes neuronales captura patrones no lineales razonablemente bien, pero no alcanza la sofisticación de las técnicas de gradient boosting o random forest.

Matriz de confusión

In [ ]:
# Matriz de confusión
mlp_cm = confusion_matrix(y_test, y_pred)
f, ax = plt.subplots(figsize=(6, 4))
sns.heatmap(mlp_cm, annot=True, linewidths=1, linecolor='black', fmt='g', cmap="BuPu", ax=ax)
plt.title('MLP - Matriz de Confusión')
plt.xlabel('Y predicho')
plt.ylabel('Y real')
plt.tight_layout()
plt.show()

image.png

La matriz de confusión muestra un balance razonable entre la detección de fraude y las falsas alarmas. De los 85 casos de fraude, identifica correctamente 38 (45% de recall) y genera 36 falsos positivos. Este rendimiento se sitúa entre los enfoques conservadores (K-NN y bosque aleatorio) y los más agresivos (regresión logística y árbol de decisión). La capacidad de la red neuronal para aprender patrones complejos le permite mantener una precisión adecuada al tiempo que detecta una buena parte de las transacciones fraudulentas, lo que la convierte en una opción viable para los sistemas de detección de fraude.

Curva ROC

In [ ]:
# Curva ROC
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba, ax=ax, name=f"MLP (AUC = {auc:.3f})", color='lightseagreen')
ax.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Azar (AUC = 0.5)')
ax.set_title("Curva ROC - MLP", fontsize=14)
ax.set_xlabel("Tasa de Falsos Positivos", fontsize=12)
ax.set_ylabel("Tasa de Verdaderos Positivos", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

image.png

De acuerdo a lo observado en la curva ROC, el modelo demuestra una buena capacidad discriminativa con un AUC de 0.83. La curva muestra un rendimiento estable en diferentes umbrales, lo que indica que el MLP puede distinguir eficazmente entre transacciones fraudulentas y legítimas. Sin embargo, no alcanza el rendimiento de Random Forest (0.93) y XGBoost (0.96), lo que sugiere que la arquitectura de la red neuronal podría necesitar optimización o más datos de entrenamiento para alcanzar su máximo potencial.

Resultados¶

In [ ]:
# Create the results table with improved structure
results_data = {
    'Model': ['K-NN', 'Logistic Regression', 'Bayesian Classification', 'Decision Tree', 'Random Forest', 'XGBoost', 'Multilayer Perceptron'],
    'Accuracy': [0.97, 0.97, 0.22, 0.82, 0.98, 0.99, 0.97],
    'Precision_Class_0': [0.97, 0.99, 1.00, 0.99, 0.98, 0.99, 0.98],
    'Recall_Class_0': [1.00, 0.78, 0.20, 0.82, 1.00, 1.00, 0.99],
    'F1_Score_Class_0': [0.99, 0.87, 0.33, 0.90, 0.99, 0.99, 0.99],
    'Precision_Class_1': [1.00, 0.99, 0.05, 0.10, 0.92, 0.87, 0.51],
    'Recall_Class_1': [0.02, 0.75, 0.96, 0.75, 0.42, 0.65, 0.45],
    'F1_Score_Class_1': [0.05, 0.15, 0.06, 0.18, 0.58, 0.74, 0.48],
    'Macro_Avg_Precision': [0.99, 0.54, 0.51, 0.55, 0.95, 0.93, 0.75],
    'Macro_Avg_Recall': [0.51, 0.76, 0.58, 0.79, 0.71, 0.82, 0.72],
    'Macro_Avg_F1': [0.52, 0.51, 0.20, 0.54, 0.79, 0.87, 0.73],
    'Weighted_Avg_Precision': [0.97, 0.97, 0.97, 0.97, 0.98, 0.99, 0.97],
    'Weighted_Avg_Recall': [0.97, 0.97, 0.22, 0.82, 0.98, 0.99, 0.97],
    'Weighted_Avg_F1': [0.95, 0.85, 0.33, 0.88, 0.98, 0.99, 0.97],
    'AUC': [0.8201, 0.8506, 0.6073, 0.8254, 0.9277, 0.9615, 0.8278]
}

# Create DataFrame
df_results = pd.DataFrame(results_data)

# Sort by AUC (best performance indicator for imbalanced datasets)
df_results_sorted = df_results.sort_values('AUC', ascending=False).reset_index(drop=True)

# Add ranking column
df_results_sorted.insert(0, 'Rank', range(1, len(df_results_sorted) + 1))

# Create a more formatted display function
def display_enhanced_table(df):
    print("🎯 Comparación del desempeño de los modelos de Machine Learning")
    print("=" * 100)
    print(f"📊 Dataset: Class 0 (3,850 samples) | Class 1 (85 samples) | Imbalance Ratio: 45.3:1")
    print("=" * 100)

    # Display with better formatting
    pd.set_option('display.max_columns', None)
    pd.set_option('display.width', 1000)
    pd.set_option('display.float_format', '{:.3f}'.format)

    # Create a styled version
    styled_df = df.style.format({
        'Accuracy': '{:.3f}',
        'Precision_Class_0': '{:.3f}',
        'Recall_Class_0': '{:.3f}',
        'F1_Score_Class_0': '{:.3f}',
        'Precision_Class_1': '{:.3f}',
        'Recall_Class_1': '{:.3f}',
        'F1_Score_Class_1': '{:.3f}',
        'Macro_Avg_Precision': '{:.3f}',
        'Macro_Avg_Recall': '{:.3f}',
        'Macro_Avg_F1': '{:.3f}',
        'Weighted_Avg_Precision': '{:.3f}',
        'Weighted_Avg_Recall': '{:.3f}',
        'Weighted_Avg_F1': '{:.3f}',
        'AUC': '{:.3f}'
    })

    print(df.to_string(index=False, float_format='%.3f'))

    return df

# Mostrar tabla
df_enhanced = display_enhanced_table(df_results_sorted)

# Crear tabla resumen de estadísticas
print("\n" + "=" * 100)
print("📈 Resumen de estadísticas de desempeño por modelo")
print("=" * 100)

summary_stats = pd.DataFrame({
    'Metric': ['Accuracy', 'AUC', 'Macro F1', 'Class 1 Recall', 'Class 1 F1'],
    'Best Model': [
        df_results.loc[df_results['Accuracy'].idxmax(), 'Model'],
        df_results.loc[df_results['AUC'].idxmax(), 'Model'],
        df_results.loc[df_results['Macro_Avg_F1'].idxmax(), 'Model'],
        df_results.loc[df_results['Recall_Class_1'].idxmax(), 'Model'],
        df_results.loc[df_results['F1_Score_Class_1'].idxmax(), 'Model']
    ],
    'Best Score': [
        f"{df_results['Accuracy'].max():.3f}",
        f"{df_results['AUC'].max():.3f}",
        f"{df_results['Macro_Avg_F1'].max():.3f}",
        f"{df_results['Recall_Class_1'].max():.3f}",
        f"{df_results['F1_Score_Class_1'].max():.3f}"
    ],
    'Worst Model': [
        df_results.loc[df_results['Accuracy'].idxmin(), 'Model'],
        df_results.loc[df_results['AUC'].idxmin(), 'Model'],
        df_results.loc[df_results['Macro_Avg_F1'].idxmin(), 'Model'],
        df_results.loc[df_results['Recall_Class_1'].idxmin(), 'Model'],
        df_results.loc[df_results['F1_Score_Class_1'].idxmin(), 'Model']
    ],
    'Worst Score': [
        f"{df_results['Accuracy'].min():.3f}",
        f"{df_results['AUC'].min():.3f}",
        f"{df_results['Macro_Avg_F1'].min():.3f}",
        f"{df_results['Recall_Class_1'].min():.3f}",
        f"{df_results['F1_Score_Class_1'].min():.3f}"
    ]
})

print(summary_stats.to_string(index=False))

# Create a compact comparison table for key metrics
print("\n" + "=" * 100)
print("🔍 Comparación de métricas claves (Ordenado por AUC)")
print("=" * 100)

key_metrics = df_results_sorted[['Rank', 'Model', 'Accuracy', 'AUC', 'Macro_Avg_F1', 'F1_Score_Class_1', 'Recall_Class_1']].copy()
key_metrics.columns = ['Rank', 'Model', 'Accuracy', 'AUC', 'Macro F1', 'Class 1 F1', 'Class 1 Recall']

print(key_metrics.to_string(index=False, float_format='%.3f'))

# Performance categorization
print("\n" + "=" * 100)
print("🏆 Categorización del desempeño")
print("=" * 100)

excellent = df_results_sorted[df_results_sorted['AUC'] >= 0.9]['Model'].tolist()
good = df_results_sorted[(df_results_sorted['AUC'] >= 0.8) & (df_results_sorted['AUC'] < 0.9)]['Model'].tolist()
moderate = df_results_sorted[(df_results_sorted['AUC'] >= 0.7) & (df_results_sorted['AUC'] < 0.8)]['Model'].tolist()
poor = df_results_sorted[df_results_sorted['AUC'] < 0.7]['Model'].tolist()

print(f"🥇 EXCELENTE (AUC ≥ 0.9): {', '.join(excellent) if excellent else 'None'}")
print(f"🥈 BUENO (0.8 ≤ AUC < 0.9): {', '.join(good) if good else 'None'}")
print(f"🥉 MODERADO (0.7 ≤ AUC < 0.8): {', '.join(moderate) if moderate else 'None'}")
print(f"❌ MALO (AUC < 0.7): {', '.join(poor) if poor else 'None'}")
🎯 Comparación del desempeño de los modelos de Machine Learning
====================================================================================================
📊 Dataset: Class 0 (3,850 samples) | Class 1 (85 samples) | Imbalance Ratio: 45.3:1
====================================================================================================
 Rank                   Model  Accuracy  Precision_Class_0  Recall_Class_0  F1_Score_Class_0  Precision_Class_1  Recall_Class_1  F1_Score_Class_1  Macro_Avg_Precision  Macro_Avg_Recall  Macro_Avg_F1  Weighted_Avg_Precision  Weighted_Avg_Recall  Weighted_Avg_F1   AUC
    1                 XGBoost     0.990              0.990           1.000             0.990              0.870           0.650             0.740                0.930             0.820         0.870                   0.990                0.990            0.990 0.962
    2           Random Forest     0.980              0.980           1.000             0.990              0.920           0.420             0.580                0.950             0.710         0.790                   0.980                0.980            0.980 0.928
    3     Logistic Regression     0.970              0.990           0.780             0.870              0.990           0.750             0.150                0.540             0.760         0.510                   0.970                0.970            0.850 0.851
    4   Multilayer Perceptron     0.970              0.980           0.990             0.990              0.510           0.450             0.480                0.750             0.720         0.730                   0.970                0.970            0.970 0.828
    5           Decision Tree     0.820              0.990           0.820             0.900              0.100           0.750             0.180                0.550             0.790         0.540                   0.970                0.820            0.880 0.825
    6                    K-NN     0.970              0.970           1.000             0.990              1.000           0.020             0.050                0.990             0.510         0.520                   0.970                0.970            0.950 0.820
    7 Bayesian Classification     0.220              1.000           0.200             0.330              0.050           0.960             0.060                0.510             0.580         0.200                   0.970                0.220            0.330 0.607

====================================================================================================
📈 Resumen de estadísticas de desempeño por modelo
====================================================================================================
        Metric              Best Model Best Score             Worst Model Worst Score
      Accuracy                 XGBoost      0.990 Bayesian Classification       0.220
           AUC                 XGBoost      0.962 Bayesian Classification       0.607
      Macro F1                 XGBoost      0.870 Bayesian Classification       0.200
Class 1 Recall Bayesian Classification      0.960                    K-NN       0.020
    Class 1 F1                 XGBoost      0.740                    K-NN       0.050

====================================================================================================
🔍 Comparación de métricas claves (Ordenado por AUC)
====================================================================================================
 Rank                   Model  Accuracy   AUC  Macro F1  Class 1 F1  Class 1 Recall
    1                 XGBoost     0.990 0.962     0.870       0.740           0.650
    2           Random Forest     0.980 0.928     0.790       0.580           0.420
    3     Logistic Regression     0.970 0.851     0.510       0.150           0.750
    4   Multilayer Perceptron     0.970 0.828     0.730       0.480           0.450
    5           Decision Tree     0.820 0.825     0.540       0.180           0.750
    6                    K-NN     0.970 0.820     0.520       0.050           0.020
    7 Bayesian Classification     0.220 0.607     0.200       0.060           0.960

====================================================================================================
🏆 Categorización del desempeño
====================================================================================================
🥇 EXCELENTE (AUC ≥ 0.9): XGBoost, Random Forest
🥈 BUENO (0.8 ≤ AUC < 0.9): Logistic Regression, Multilayer Perceptron, Decision Tree, K-NN
🥉 MODERADO (0.7 ≤ AUC < 0.8): None
❌ MALO (AUC < 0.7): Bayesian Classification

Basado en los resultados de 7 modelos de machine learnign, los resultados nos muestran que el modelo no se ejecuta de la manera más adecuada debido al desbalance de clases. La base de datos esta dominada por la clase 0 (3850 observaciones) y la clase 1 siendo minoritaria con (85 observaciones). Dicho esto observamos como el desbalance de clases genera, a pesar de los altos puntajes de accuracy, retos importantes para la detección de fraude. Esto sugiere que la medición tradicional de accuracy tiende a ser engañosa.

Sin embargo, podemos observar que XGBoost es el modelo con el mejor desempeño en general, con un AUC de 0.962, accuracy de 99% y un puntaje F1 de 0.87. Est modelo muestra el mejor balance entre clases con un puntaje de F1 de 0.99 para la clase 0 y 0.74 para la clase 1. Random Forest es el modelo con el segundo mejor desempeño, con un AUC de 0.928, accuracy de 98% y un puntaje F1 de 0.79, mostrando un buen desempeño en las diferentes métricas. Finalmente, el tercer modelo con mejor desempeño es el Multileyer Perceptron. El modelo arroja resultados de un AUC de 0.828, accuracy de 97% y puntaje F1 de 0.73.

La base de datos muestra un alto desbalance de clases, creando algunos retos para el análisis. La mayoría de los modelos alcanzan puntajes de accuracy por encima del 97% prediciendo principalmente la clase mayoritaria, haciendo que sea una medición engañosa para evaluar el verdadero poder predictivo del modelo. El desbalance en las clases afecta la detección de la clase minoritaria, con la mayoría de modelos teniendo dificultades para predecir adecuadamente la clase 1 o el fraude. Este problema se evidencia de manera clara en el modelo de Clasificación Bayesiana, el cuál falla con un accuracy del 22%, sugiriendo un sobreajuste en la clase mayoritaria y realtando la ineficiencia de los métodos probabilisticos tradicionales para conjuntos de datos altamente desbalanceados.

Para este conjunto de datos en particular, XGBoost debería ser la opción principal debido a su excelente gestión del desequilibrio de clases y su capacidad de discriminación. Su enfoque de conjunto y la regularización integrada lo hacen especialmente eficaz para conjuntos de datos desequilibrados. Random Forest es una excelente alternativa, ofreciendo buena interpretabilidad y robustez, a la vez que mantiene un rendimiento razonable en ambas clases. Entre los modelos que se deben evitar se incluyen la clasificación bayesiana, que presenta una incompatibilidad con la estructura de este conjunto de datos, y K-NN, que muestra una excesiva sensibilidad al desequilibrio de clases.

Sin lugar a dudas lo más importante sería implementar técnicas para mejorar el desbalance de clases. Como se mencionó con anterioridad, no se aplicó OneHotEncoder ni VIF debido a la alta dimensionalidad del dataset y a que el cálculo de VIF no se pudo completar por limitaciones computacionales. Más aún, para la evaluación se debe priorizar el AUC, el F1-score y recall para la clase 1 sobre las métricas de precisión simples, ya que estas reflejan mejor el rendimiento del modelo en conjuntos de datos desequilibrados.

In [ ]:
# Create visualizations
fig, axes = plt.subplots(2, 2, figsize=(15, 12))

# 1. Accuracy comparison
axes[0, 0].bar(df_results['Model'], df_results['Accuracy'], color='skyblue')
axes[0, 0].set_title('Model Accuracy Comparison')
axes[0, 0].set_ylabel('Accuracy')
axes[0, 0].tick_params(axis='x', rotation=45)
axes[0, 0].set_ylim(0, 1)

# 2. AUC comparison
axes[0, 1].bar(df_results['Model'], df_results['AUC'], color='lavender')
axes[0, 1].set_title('Model AUC Comparison')
axes[0, 1].set_ylabel('AUC')
axes[0, 1].tick_params(axis='x', rotation=45)
axes[0, 1].set_ylim(0, 1)

# 3. F1-Score comparison for both classes
x = range(len(df_results['Model']))
width = 0.35
axes[1, 0].bar([i - width/2 for i in x], df_results['F1_Score_Class_0'], width, label='Class 0', color='darkorange', alpha=0.7)
axes[1, 0].bar([i + width/2 for i in x], df_results['F1_Score_Class_1'], width, label='Class 1', color='lightcoral', alpha=0.7)
axes[1, 0].set_title('F1-Score Comparison by Class')
axes[1, 0].set_ylabel('F1-Score')
axes[1, 0].set_xticks(x)
axes[1, 0].set_xticklabels(df_results['Model'], rotation=45)
axes[1, 0].legend()
axes[1, 0].set_ylim(0, 1)

# 4. Macro Average F1-Score
axes[1, 1].bar(df_results['Model'], df_results['Macro_Avg_F1'], color='orchid', alpha=0.7)
axes[1, 1].set_title('Macro Average F1-Score Comparison')
axes[1, 1].set_ylabel('Macro Avg F1-Score')
axes[1, 1].tick_params(axis='x', rotation=45)
axes[1, 1].set_ylim(0, 1)

plt.tight_layout()
plt.show()

# Summary statistics
print("\n" + "=" * 80)
print("Mejores Resultados por métrica:")
print("=" * 80)
print(f"Accuracy más alto: {df_results.loc[df_results['Accuracy'].idxmax(), 'Model']} ({df_results['Accuracy'].max():.3f})")
print(f"AUC más alto: {df_results.loc[df_results['AUC'].idxmax(), 'Model']} ({df_results['AUC'].max():.3f})")
print(f"Macro F1 más alto: {df_results.loc[df_results['Macro_Avg_F1'].idxmax(), 'Model']} ({df_results['Macro_Avg_F1'].max():.3f})")
print(f"Mejor Clase 1 F1: {df_results.loc[df_results['F1_Score_Class_1'].idxmax(), 'Model']} ({df_results['F1_Score_Class_1'].max():.3f})")
No description has been provided for this image
================================================================================
Mejores Resultados por métrica:
================================================================================
Accuracy más alto: XGBoost (0.990)
AUC más alto: XGBoost (0.962)
Macro F1 más alto: XGBoost (0.870)
Mejor Clase 1 F1: XGBoost (0.740)

Comparación Accuracy

La comparación del accuracy revela una uniformidad engañosa entre la mayoría de los modelos ya que seis de siete alcanzan puntuaciones de precisión entre el 97 y el 99 %. Solo el modelo de Clasificación Bayesiana muestra una precisión significativamente menor, de aproximadamente el 22 %, mientras que el Árbol de Decisión tiene un rendimiento moderado, del 82 %. Este aparente alto rendimiento en la mayoría de los modelos resulta engañoso en el contexto de un conjunto de datos con desequilibrio de clases, donde lograr una precisión del 97,8 % solo requiere predecir siempre la clase mayoritaria. La similitud en las puntuaciones de precisión oculta diferencias significativas en la capacidad real del modelo y en el rendimiento de detección de la clase minoritaria.

Comparación AUC

La comparación de los resultados del AUC muestran información más confiable sobre el rendimiento de los modelos para este conjunto de datos. XGBoost demuestra una capacidad de discriminación ente clases alta con un AUC de aproximadamente 0,96, seguido por Random Forest con 0,93, lo que indica una buena capacidad para distinguir entre clases. La mayoría de los demás modelos se agrupan en el rango moderado, entre 0,82 y 0,85, lo que sugiere un rendimiento aceptable, pero no excepcional. El modelo de clasificación bayesiana falla por completo con un AUC cercano a 0,61

Comparación F1-Score por clase

Este gráfico de dos barras ilustra claramente el problema del desequilibrio de clases al mostrar niveles de rendimiento diferentes entre ellas. Las barras naranjas que representan la clase 0 (clase mayoritaria) muestran puntuaciones F1 uniformemente altas, superiores a 0,8, para la mayoría de los modelos, lo que refleja la facilidad para predecir la clase dominante. En contraste, las barras rojizas de la clase 1 (clase minoritaria) revelan un rendimiento bajo para la mayoría de los modelos, donde K-NN prácticamente no detecta clases minoritarias y la mayoría de los demás no superan 0,5. Solo XGBoost y Random Forest muestran puntuaciones F1 razonables para la clase 1, de 0,74 y 0,58, respectivamente.

Comparación F1-Score (Promedio)

Los resultados del F1-score proporcionan una evaluación más equilibrada y honesta al ponderar por igual ambas clases. En línea con los resultados del gráfico anterior, XGBoost sigue obteniendo los mejores resultados con 0.87, seguido de Random Forest con 0.79, lo que demuestra la capacidad de mantener un rendimiento razonable en ambas clases. Todos los demás modelos muestran un rendimiento equilibrado por debajo de 0.73 y la clasificación bayesiana presenta el puntaje más bajo con 0.20. Esta métrica expone como los modelos que parecian buenos únicamente en su puntaje de accuracy, en realidad fallan cuando se les exige un rendimiento bueno en ambas clases.

Ejercicio No. 3. Modelos de Regresión¶

In [ ]:
# Variables seleccionadas para el modelo reducido
selected_features = [
    'vento_rajada_max',
    'pressao_horaria',
    'temp_max_ant',
    'umid_horaria',
    'precipitacao'
]

El conjunto de variables seleccionadas para predecir la velocidad del viento está conformado por aquellas que, tanto desde una perspectiva estadística como desde una lógica meteorológica, aportan valor explicativo y predictivo al modelo. Este set incluye: vento_rajada_max, pressao_horaria, temp_max_ant, umid_horaria y precipitacao.

La variable vento_rajada_max es el predictor más fuerte, con una alta correlación directa con la velocidad del viento. Su inclusión está plenamente justificada, ya que las ráfagas máximas y la velocidad media del viento están íntimamente relacionadas. Además de su relevancia estadística, esta variable tiene un claro sustento físico, pues ambas medidas responden a los mismos fenómenos atmosféricos.

Por su parte, pressao_horaria representa uno de los principales determinantes físicos del viento. Las diferencias de presión atmosférica generan gradientes que impulsan el movimiento del aire. Aunque su correlación directa con la velocidad del viento es moderada, su valor reside en que es un mecanismo causal subyacente clave, especialmente útil para construir modelos que respeten principios estructurales.

La temp_max_ant, o temperatura máxima en la hora anterior, también se incluye por su influencia en los gradientes térmicos que afectan la circulación del aire. Entre las medidas de temperatura disponibles, se eligió esta variable por su mejor correlación con la variable objetivo y para evitar redundancia, ya que la temperatura mínima presenta una correlación muy alta con la máxima, lo que puede generar problemas de colinealidad.

En cuanto a la umid_horaria, aunque presenta una correlación negativa moderada con la velocidad del viento, su incorporación está justificada porque puede capturar condiciones atmosféricas asociadas a estabilidad o inestabilidad. En contextos de alta humedad, es más probable que el ambiente sea estable y, por tanto, con menor viento, lo cual añade una capa útil de información contextual al modelo.

Finalmente, se incluye la variable precipitacao, a pesar de su baja correlación individual con la velocidad del viento. Esta decisión se fundamenta en su posible utilidad para detectar eventos extremos, como tormentas, que pueden estar acompañados de vientos fuertes. En modelos no lineales, precipitacao puede interactuar con otras variables como la presión o la temperatura y aportar valor adicional que no se capta en modelos lineales simples.

En conjunto, estas variables forman un set robusto, parsimonioso y flexible, apropiado tanto para modelos interpretables como para algoritmos más complejos orientados a maximizar el rendimiento predictivo.

Particiones de tiempo¶

In [ ]:
def generar_splits_temporales(df, dias_entrenamiento, dias_validacion, selected_features=None, timesteps=0):
    horas_por_dia = 24
    ventanas = []
    total_dias = len(df) // horas_por_dia
    # Ensure validation window is large enough for timesteps
    min_val_days = (timesteps // horas_por_dia) + (1 if timesteps % horas_por_dia > 0 else 0)
    if dias_validacion < min_val_days:
        print(f"Warning: Increased validation window to at least {min_val_days} days to accommodate {timesteps} timesteps.")
        dias_validacion = min_val_days

    for i in range(total_dias - dias_entrenamiento - dias_validacion + 1):
        start_train = i * horas_por_dia
        end_train = (i + dias_entrenamiento) * horas_por_dia
        start_val = end_train
        end_val = start_val + dias_validacion * horas_por_dia

        train_df = df.iloc[start_train:end_train]
        val_df = df.iloc[start_val:end_val]

        if selected_features:
            X_train = train_df[selected_features]
            X_val = val_df[selected_features]
        else:
            X_train = train_df.drop(columns=['vento_velocidade', 'hora'], errors='ignore')
            X_val = val_df.drop(columns=['vento_velocidade', 'hora'], errors='ignore')

        y_train = train_df['vento_velocidade']
        y_val = val_df['vento_velocidade']

        ventanas.append((X_train, y_train, X_val, y_val))
    return ventanas
In [ ]:
# Crear splits para T=7
splits_T7 = generar_splits_temporales(df, 7, 1, selected_features=selected_features)

# Get the first split to print shapes
X_train_wind, y_train_wind, X_val_wind, y_val_wind = splits_T7[0]

print("📊 Tamaños del primer split (T=7 días + 1 validación):")
print(f"X_train_wind: {X_train_wind.shape}, y_train_wind: {y_train_wind.shape}")
print(f"X_val_wind:   {X_val_wind.shape}, y_val_wind:   {y_val_wind.shape}")
📊 Tamaños del primer split (T=7 días + 1 validación):
X_train_wind: (168, 5), y_train_wind: (168,)
X_val_wind:   (24, 5), y_val_wind:   (24,)
In [ ]:
splits_T14 = generar_splits_temporales(df, 14, 1, selected_features=selected_features)

print("📊 Tamaños del primer split (T=14 días + 1 validación):")
print(f"X_train_wind: {X_train_wind.shape}, y_train_wind: {y_train_wind.shape}")
print(f"X_val_wind:   {X_val_wind.shape}, y_val_wind:   {y_val_wind.shape}")
📊 Tamaños del primer split (T=14 días + 1 validación):
X_train_wind: (168, 5), y_train_wind: (168,)
X_val_wind:   (24, 5), y_val_wind:   (24,)
In [ ]:
splits_T21 = generar_splits_temporales(df, 21, 1, selected_features=selected_features)

print("📊 Tamaños del primer split (T=21 días + 1 validación):")
print(f"X_train_wind: {X_train_wind.shape}, y_train_wind: {y_train_wind.shape}")
print(f"X_val_wind:   {X_val_wind.shape}, y_val_wind:   {y_val_wind.shape}")
📊 Tamaños del primer split (T=21 días + 1 validación):
X_train_wind: (168, 5), y_train_wind: (168,)
X_val_wind:   (24, 5), y_val_wind:   (24,)
In [ ]:
def calcular_mape_seguro(y_true, y_pred):
    y_true_safe = np.where(y_true == 0, 0.01, y_true)
    return np.mean(np.abs((y_true - y_pred) / y_true_safe)) * 100

a. K-NN¶

In [ ]:
def entrenar_knn_splits(splits, nombre_ventana):
    metricas_knn = []

    for X_train_wind, y_train_wind, X_val_wind, y_val_wind in splits:
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train_wind)
        X_val_scaled = scaler.transform(X_val_wind)

        modelo_knn = KNeighborsRegressor(n_neighbors=5)
        modelo_knn.fit(X_train_scaled, y_train_wind)
        y_pred = modelo_knn.predict(X_val_scaled)

        mae = mean_absolute_error(y_val_wind, y_pred)
        mape = calcular_mape_seguro(y_val_wind, y_pred)
        mse = mean_squared_error(y_val_wind, y_pred)
        rmse = np.sqrt(mse)

        # Test de autocorrelación de residuos
        residuos = y_val_wind - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_knn.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_knn)

# Ejecutar para T=7, T=14, T=21
df_knn_T7 = entrenar_knn_splits(splits_T7, "T=7")
df_knn_T14 = entrenar_knn_splits(splits_T14, "T=14")
df_knn_T21 = entrenar_knn_splits(splits_T21, "T=21")

# Combinar y calcular promedios
resultados_knn = pd.concat([df_knn_T7, df_knn_T14, df_knn_T21])
promedios_knn = resultados_knn.groupby("Ventana").mean().reset_index()

print("\n Promedios del modelo K-NN Regressor por ventana:")
print(promedios_knn.round(4))
 Promedios del modelo K-NN Regressor por ventana:
  Ventana     MAE      MAPE     MSE    RMSE  p_valor_LjungBox
0    T=14  0.5459  246.2973  0.5067  0.6907            0.3757
1    T=21  0.5312  238.0413  0.4815  0.6739            0.3948
2     T=7  0.5807  267.8269  0.5705  0.7308            0.3338

El modelo K-NN muestra mejor desempeño en la ventana de T=21, obteniendo los menores errores (MAE: 0.5312, RMSE: 0.6739) y el menor MAPE (238.04), lo que indica mayor precisión al capturar patrones de velocidad del viento. En contraste, T=7 presenta los mayores errores, evidenciando que ventanas cortas no logran capturar suficiente contexto temporal.

Adicionalmente, los p-valores del test de Ljung-Box son todos superiores a 0.33, indicando que los residuos no tienen autocorrelación significativa, por tanto, las predicciones son temporalmente independientes. Aunque el MAPE es alto en general, podría deberse a valores reales cercanos a cero.

b. Regresión Lineal¶

In [ ]:
def entrenar_lr_splits(splits, nombre_ventana):
    metricas_lr = []

    for X_train_wind, y_train_wind, X_val_wind, y_val_wind in splits:
        # Escalar datos
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train_wind)
        X_val_scaled = scaler.transform(X_val_wind)

        # Modelo de Regresión Lineal
        modelo_lr = LinearRegression()
        modelo_lr.fit(X_train_scaled, y_train_wind)
        y_pred = modelo_lr.predict(X_val_scaled)

        # Métricas
        mae = mean_absolute_error(y_val_wind, y_pred)
        mape = calcular_mape_seguro(y_val_wind, y_pred)
        mse = mean_squared_error(y_val_wind, y_pred)
        rmse = np.sqrt(mse)

        # Test de Ljung-Box
        residuos = y_val_wind - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_lr.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_lr)

# Entrenar modelo para cada ventana
df_lr_T7 = entrenar_lr_splits(splits_T7, "T=7")
df_lr_T14 = entrenar_lr_splits(splits_T14, "T=14")
df_lr_T21 = entrenar_lr_splits(splits_T21, "T=21")

# Combinar y calcular promedios
resultados_lr = pd.concat([df_lr_T7, df_lr_T14, df_lr_T21])
promedios_lr = resultados_lr.groupby("Ventana").mean().reset_index()

print("\n Promedios del modelo de Regresión Lineal por ventana:")
print(promedios_lr.round(4))
 Promedios del modelo de Regresión Lineal por ventana:
  Ventana     MAE      MAPE     MSE    RMSE  p_valor_LjungBox
0    T=14  0.4749  197.6838  0.5559  0.6203            0.4468
1    T=21  0.4671  196.8841  0.3985  0.6028            0.4485
2     T=7  0.4887  198.7880  0.6773  0.6458            0.4387

El modelo de Regresión Lineal también obtiene su mejor desempeño con una ventana de T=21, presentando el menor MAE (0.4671), MSE (0.3985) y RMSE (0.6028), además del MAPE más bajo (196.88), lo que refleja predicciones más precisas en comparación con las otras ventanas. La ventana T=7 es nuevamente la menos efectiva, con errores más altos.

Los p-valores del test de Ljung-Box superan 0.43 en todos los casos, lo que indica residuos sin autocorrelación significativa, por lo que, las predicciones son fiables en el tiempo. La regresión lineal muestra menor error que el modelo K-NN en todos los indicadores, sugiriendo que, en este caso, un enfoque lineal con ventana T=21 es más adecuado para la predicción de velocidad del viento.

c. Modelo Ridge¶

In [ ]:
def entrenar_ridge_splits(splits, nombre_ventana, alpha=1.0):
    metricas_ridge = []

    for X_train, y_train, X_val, y_val in splits:
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)
        X_val_scaled = scaler.transform(X_val)

        modelo_ridge = Ridge(alpha=alpha)
        modelo_ridge.fit(X_train_scaled, y_train)
        y_pred = modelo_ridge.predict(X_val_scaled)

        mae = mean_absolute_error(y_val, y_pred)
        mape = calcular_mape_seguro(y_val, y_pred)
        mse = mean_squared_error(y_val, y_pred)
        rmse = np.sqrt(mse)

        residuos = y_val - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_ridge.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_ridge)


# Ejemplo de ejecución para las mismas ventanas T=7, T=14, T=21
df_ridge_T7 = entrenar_ridge_splits(splits_T7, "T=7", alpha=1.0)
df_ridge_T14 = entrenar_ridge_splits(splits_T14, "T=14", alpha=1.0)
df_ridge_T21 = entrenar_ridge_splits(splits_T21, "T=21", alpha=1.0)

resultados_ridge = pd.concat([df_ridge_T7, df_ridge_T14, df_ridge_T21])
promedios_ridge = resultados_ridge.groupby("Ventana").mean().reset_index()

print("\n Promedios del modelo Ridge Regressor por ventana:")
print(promedios_ridge.round(4))
 Promedios del modelo Ridge Regressor por ventana:
  Ventana     MAE      MAPE     MSE    RMSE  p_valor_LjungBox
0    T=14  0.4749  198.7291  0.5539  0.6201            0.4463
1    T=21  0.4672  197.5445  0.3985  0.6027            0.4481
2     T=7  0.4881  200.8995  0.6643  0.6443            0.4382

El modelo Ridge presenta su mejor rendimiento con la ventana T=21, logrando los menores errores (MAE: 0.4672, RMSE: 0.6027) y el MAPE más bajo (197.54), muy similar al modelo de Regresión Lineal. La ventana T=7 nuevamente arroja los mayores errores, lo que refuerza la importancia de incluir más contexto temporal en la predicción.

Los p-valores del test de Ljung-Box son todos mayores a 0.43, lo que indica ausencia de autocorrelación en los residuos. Dado que Ridge regulariza la regresión lineal, su rendimiento prácticamente idéntico sugiere que no hay sobreajuste relevante y que ambos modelos lineales con T=21 son buenas opciones para predecir la velocidad del viento.

d. Modelo Lasso¶

In [ ]:
def entrenar_lasso_splits(splits, nombre_ventana, alpha=1.0):
    metricas_lasso = []

    for X_train, y_train, X_val, y_val in splits:
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)
        X_val_scaled = scaler.transform(X_val)

        modelo_lasso = Lasso(alpha=alpha, max_iter=10000)
        modelo_lasso.fit(X_train_scaled, y_train)
        y_pred = modelo_lasso.predict(X_val_scaled)

        mae = mean_absolute_error(y_val, y_pred)
        mape = calcular_mape_seguro(y_val, y_pred)
        mse = mean_squared_error(y_val, y_pred)
        rmse = np.sqrt(mse)

        residuos = y_val - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_lasso.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_lasso)


# Ejemplo de ejecución para las ventanas T=7, T=14, T=21
df_lasso_T7 = entrenar_lasso_splits(splits_T7, "T=7", alpha=1.0)
df_lasso_T14 = entrenar_lasso_splits(splits_T14, "T=14", alpha=1.0)
df_lasso_T21 = entrenar_lasso_splits(splits_T21, "T=21", alpha=1.0)

resultados_lasso = pd.concat([df_lasso_T7, df_lasso_T14, df_lasso_T21])
promedios_lasso = resultados_lasso.groupby("Ventana").mean().reset_index()

print("\n Promedios del modelo Lasso Regressor por ventana:")
print(promedios_lasso.round(4))
 Promedios del modelo Lasso Regressor por ventana:
  Ventana     MAE      MAPE     MSE    RMSE  p_valor_LjungBox
0    T=14  0.9853  630.2549  1.4965  1.1843            0.0597
1    T=21  0.9772  627.0319  1.4694  1.1741            0.0612
2     T=7  0.9915  622.4506  1.5252  1.1928            0.0574

El modelo Lasso Regressor muestra un desempeño significativamente inferior al de los demás modelos, con errores mucho más altos (MAE cercano a 1.0 y MAPE superior a 620%) en todas las ventanas. Aunque T=21 vuelve a ser ligeramente mejor, las diferencias entre ventanas son mínimas y el nivel general de error indica que este modelo no se ajusta bien al problema de predicción.

Además, los p-valores del test de Ljung-Box están cerca del umbral de significancia (≈0.06), lo que sugiere una posible autocorrelación en los residuos, afectando la fiabilidad temporal del modelo. En este contexto, Lasso no es adecuado para la predicción de velocidad del viento y podría estar penalizando excesivamente las variables, perdiendo información importante.

e. Arbol de decisión¶

In [ ]:
def entrenar_decision_tree_splits(splits, nombre_ventana, max_depth=None, random_state=42):
    metricas_dt = []

    for X_train, y_train, X_val, y_val in splits:
        # Para árboles no es obligatorio escalar, pero puedes hacerlo si quieres
        modelo_dt = DecisionTreeRegressor(max_depth=max_depth, random_state=random_state)
        modelo_dt.fit(X_train, y_train)
        y_pred = modelo_dt.predict(X_val)

        mae = mean_absolute_error(y_val, y_pred)
        mape = calcular_mape_seguro(y_val, y_pred)
        mse = mean_squared_error(y_val, y_pred)
        rmse = np.sqrt(mse)

        residuos = y_val - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_dt.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_dt)


# Ejemplo de ejecución para ventanas T=7, T=14, T=21
df_dt_T7 = entrenar_decision_tree_splits(splits_T7, "T=7", max_depth=5)
df_dt_T14 = entrenar_decision_tree_splits(splits_T14, "T=14", max_depth=5)
df_dt_T21 = entrenar_decision_tree_splits(splits_T21, "T=21", max_depth=5)

resultados_dt = pd.concat([df_dt_T7, df_dt_T14, df_dt_T21])
promedios_dt = resultados_dt.groupby("Ventana").mean().reset_index()

print("\n Promedios del modelo Decision Tree Regressor por ventana:")
print(promedios_dt.round(4))
 Promedios del modelo Decision Tree Regressor por ventana:
  Ventana     MAE      MAPE     MSE    RMSE  p_valor_LjungBox
0    T=14  0.5423  187.8660  0.5308  0.7040            0.4322
1    T=21  0.5231  185.4895  0.4944  0.6787            0.4485
2     T=7  0.5893  201.5786  0.6258  0.7624            0.4166

El modelo Decision Tree Regressor alcanza su mejor desempeño con ventana T=21, con el MAE más bajo (0.5231), menor MAPE (185.49) y menor RMSE (0.6787). Si bien no es tan preciso como los modelos lineales, supera al K-NN y al Lasso. La ventana T=7 vuelve a ser la menos eficiente, reflejando errores más altos.

Los p-valores de Ljung-Box en todas las ventanas son superiores a 0.41, lo que indica residuos sin autocorrelación significativa. En resumen, el árbol de decisión con T=21 ofrece un compromiso entre flexibilidad y precisión aceptable, aunque no supera a los modelos lineales en este caso.

f. Random Forest¶

In [ ]:
# def entrenar_random_forest_splits(splits, nombre_ventana, n_estimators=50, max_depth=5, random_state=42, n_jobs=-1):
#     metricas_rf = []

#     for X_train, y_train, X_val, y_val in splits:
#         # Escalar no es estrictamente necesario para Random Forest
#         modelo_rf = RandomForestRegressor(n_estimators=n_estimators, max_depth=max_depth, random_state=random_state)
#         modelo_rf.fit(X_train, y_train)
#         y_pred = modelo_rf.predict(X_val)

#         mae = mean_absolute_error(y_val, y_pred)
#         mape = calcular_mape_seguro(y_val, y_pred)
#         mse = mean_squared_error(y_val, y_pred)
#         rmse = np.sqrt(mse)

#         residuos = y_val - y_pred
#         lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
#         p_valor = lb_test['lb_pvalue'].values[0]

#         metricas_rf.append({
#             "Ventana": nombre_ventana,
#             "MAE": mae,
#             "MAPE": mape,
#             "MSE": mse,
#             "RMSE": rmse,
#             "p_valor_LjungBox": p_valor
#         })

#     return pd.DataFrame(metricas_rf)


# # Ejemplo de ejecución para ventanas T=7, T=14, T=21
# df_rf_T7 = entrenar_random_forest_splits(splits_T7, "T=7", n_estimators=50, max_depth=5)
# df_rf_T14 = entrenar_random_forest_splits(splits_T14, "T=14", n_estimators=50, max_depth=5)
# df_rf_T21 = entrenar_random_forest_splits(splits_T21, "T=21", n_estimators=50, max_depth=5)

# resultados_rf = pd.concat([df_rf_T7, df_rf_T14, df_rf_T21])
# promedios_rf = resultados_rf.groupby("Ventana").mean().reset_index()

# print("\n Promedios del modelo Random Forest Regressor por ventana:")
# print(promedios_rf.round(4))

g. XGBoost¶

In [ ]:
def entrenar_xgboost_splits(splits, nombre_ventana, n_estimators=100, max_depth=6, learning_rate=0.1, random_state=42):
    metricas_xgb = []

    for X_train, y_train, X_val, y_val in splits:
        # Escalar no es obligatorio para XGBoost
        modelo_xgb = xgb.XGBRegressor(
            n_estimators=n_estimators,
            max_depth=max_depth,
            learning_rate=learning_rate,
            random_state=random_state,
            verbosity=0
        )
        modelo_xgb.fit(X_train, y_train)
        y_pred = modelo_xgb.predict(X_val)

        mae = mean_absolute_error(y_val, y_pred)
        mape = calcular_mape_seguro(y_val, y_pred)
        mse = mean_squared_error(y_val, y_pred)
        rmse = np.sqrt(mse)

        residuos = y_val - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_xgb.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_xgb)


# Ejemplo de ejecución para ventanas T=7, T=14, T=21
df_xgb_T7 = entrenar_xgboost_splits(splits_T7, "T=7", n_estimators=100, max_depth=6, learning_rate=0.1)
df_xgb_T14 = entrenar_xgboost_splits(splits_T14, "T=14", n_estimators=100, max_depth=6, learning_rate=0.1)
df_xgb_T21 = entrenar_xgboost_splits(splits_T21, "T=21", n_estimators=100, max_depth=6, learning_rate=0.1)

resultados_xgb = pd.concat([df_xgb_T7, df_xgb_T14, df_xgb_T21])
promedios_xgb = resultados_xgb.groupby("Ventana").mean().reset_index()

print("\n Promedios del modelo XGBoost Regressor por ventana:")
print(promedios_xgb.round(4))
 Promedios del modelo XGBoost Regressor por ventana:
  Ventana     MAE      MAPE     MSE    RMSE  p_valor_LjungBox
0    T=14  0.5282  201.3237  0.4858  0.6766            0.4332
1    T=21  0.5133  197.8739  0.4621  0.6595            0.4418
2     T=7  0.5587  220.4712  0.5427  0.7138            0.4092

El modelo XGBoost similar a los modelos anteriores muestra su mejor desempeño en la ventana T=21, donde obtiene los menores errores (MAE: 0.5133, RMSE: 0.6595) y el MAPE más bajo (197.87). Aunque no supera a los modelos lineales, ofrece una mejor capacidad de modelado no lineal y se comporta consistentemente mejor que K-NN, Lasso y el árbol de decisión.

Los p-valores de Ljung-Box, todos por encima de 0.40, indican independencia temporal en los residuos. Esto sugiere que XGBoost con T=21 es una opción robusta para capturar relaciones complejas sin comprometer la estabilidad temporal, siendo una alternativa viable en escenarios no lineales.

h. SVM Regressor¶

In [ ]:
# def entrenar_svr_splits(splits, nombre_ventana, kernel='rbf', C=1.0, epsilon=0.1):
#     metricas_svr = []

#     for X_train, y_train, X_val, y_val in splits:
#         scaler = StandardScaler()
#         X_train_scaled = scaler.fit_transform(X_train)
#         X_val_scaled = scaler.transform(X_val)

#         modelo_svr = SVR(kernel=kernel, C=C, epsilon=epsilon)
#         modelo_svr.fit(X_train_scaled, y_train)
#         y_pred = modelo_svr.predict(X_val_scaled)

#         mae = mean_absolute_error(y_val, y_pred)
#         mape = calcular_mape_seguro(y_val, y_pred)
#         mse = mean_squared_error(y_val, y_pred)
#         rmse = np.sqrt(mse)

#         residuos = y_val - y_pred
#         lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
#         p_valor = lb_test['lb_pvalue'].values[0]

#         metricas_svr.append({
#             "Ventana": nombre_ventana,
#             "MAE": mae,
#             "MAPE": mape,
#             "MSE": mse,
#             "RMSE": rmse,
#             "p_valor_LjungBox": p_valor
#         })

#     return pd.DataFrame(metricas_svr)


# # Ejemplo de ejecución para ventanas T=7, T=14, T=21
# df_svr_T7 = entrenar_svr_splits(splits_T7, "T=7", kernel='rbf', C=1.0, epsilon=0.1)
# df_svr_T14 = entrenar_svr_splits(splits_T14, "T=14", kernel='rbf', C=1.0, epsilon=0.1)
# df_svr_T21 = entrenar_svr_splits(splits_T21, "T=21", kernel='rbf', C=1.0, epsilon=0.1)

# resultados_svr = pd.concat([df_svr_T7, df_svr_T14, df_svr_T21])
# promedios_svr = resultados_svr.groupby("Ventana").mean().reset_index()

# print("\nPromedios del modelo SVR Regressor por ventana:")
# print(promedios_svr.round(4))

i. Multi-Layer Perceptron¶

In [ ]:
# def entrenar_mlp_splits(splits, nombre_ventana, hidden_layer_sizes=(100,), activation='relu', solver='adam', max_iter=500, random_state=42):
#     metricas_mlp = []

#     for X_train, y_train, X_val, y_val in splits:
#         scaler = StandardScaler()
#         X_train_scaled = scaler.fit_transform(X_train)
#         X_val_scaled = scaler.transform(X_val)

#         modelo_mlp = MLPRegressor(
#             hidden_layer_sizes=(100,),
#             activation='relu',
#             solver='adam',
#             max_iter=1000,
#             random_state=42,
#             early_stopping=True,
#             n_iter_no_change=10,
#             learning_rate_init=0.0005,
#             verbose=True
#         )

#         modelo_mlp.fit(X_train_scaled, y_train)
#         y_pred = modelo_mlp.predict(X_val_scaled)

#         mae = mean_absolute_error(y_val, y_pred)
#         mape = calcular_mape_seguro(y_val, y_pred)
#         mse = mean_squared_error(y_val, y_pred)
#         rmse = np.sqrt(mse)

#         residuos = y_val - y_pred
#         lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
#         p_valor = lb_test['lb_pvalue'].values[0]

#         metricas_mlp.append({
#             "Ventana": nombre_ventana,
#             "MAE": mae,
#             "MAPE": mape,
#             "MSE": mse,
#             "RMSE": rmse,
#             "p_valor_LjungBox": p_valor
#         })

#     return pd.DataFrame(metricas_mlp)


# # Ejecución para las tres ventanas
# df_mlp_T7 = entrenar_mlp_splits(splits_T7, "T=7", hidden_layer_sizes=(100,), max_iter=500)
# df_mlp_T14 = entrenar_mlp_splits(splits_T14, "T=14", hidden_layer_sizes=(100,), max_iter=500)
# df_mlp_T21 = entrenar_mlp_splits(splits_T21, "T=21", hidden_layer_sizes=(100,), max_iter=500)

# resultados_mlp = pd.concat([df_mlp_T7, df_mlp_T14, df_mlp_T21])
# promedios_mlp = resultados_mlp.groupby("Ventana").mean().reset_index()

# print("\nPromedios del modelo MLP Regressor por ventana:")
# print(promedios_mlp.round(4))

j. RNN¶

1. Crear secuencias de tiempo¶

In [ ]:
# def preparar_datos_rnn(X, y, timesteps=24):
#     X = X.copy()
#     y = y.reset_index(drop=True)

#     num_samples = len(X) - timesteps
#     if num_samples <= 0:
#         raise ValueError(f"Not enough samples: got {len(X)}, need at least {timesteps + 1}")

#     X_seq = np.zeros((num_samples, timesteps, X.shape[1]))
#     y_seq = np.zeros((num_samples,))

#     X_values = X.values

#     for i in range(num_samples):
#         X_seq[i] = X_values[i:i + timesteps]
#         y_seq[i] = y.iloc[i + timesteps]

#     return X_seq, y_seq

2. Entrenamiento de la red neuronal¶

In [ ]:
# def entrenar_rnn_splits(splits, nombre_ventana, timesteps=24, units=50, epochs=20, batch_size=32, verbose=0):
#     metricas_rnn = []

#     for X_train, y_train, X_val, y_val in splits:
#         # Ensure column consistency
#         X_val = X_val[X_train.columns]

#         # Normalizar
#         scaler = StandardScaler()
#         X_train_scaled = scaler.fit_transform(X_train)
#         X_val_scaled = scaler.transform(X_val)

#         # Reconstruir DataFrames para mantener columnas
#         X_train_df = pd.DataFrame(X_train_scaled, columns=X_train.columns)
#         X_val_df = pd.DataFrame(X_val_scaled, columns=X_val.columns)

#         # Convertir a secuencias
#         X_train_seq, y_train_seq = preparar_datos_rnn(X_train_df, y_train.reset_index(drop=True), timesteps)
#         X_val_seq, y_val_seq = preparar_datos_rnn(X_val_df, y_val.reset_index(drop=True), timesteps)

#         # Construir modelo RNN
#         model = Sequential()
#         model.add(Input(shape=(timesteps, X_train_seq.shape[2])))
#         model.add(SimpleRNN(units, activation='tanh'))
#         model.add(Dense(1))
#         model.compile(optimizer=Adam(), loss='mse')

#         model.fit(X_train_seq, y_train_seq, epochs=epochs, batch_size=batch_size, verbose=verbose)

#         y_pred = model.predict(X_val_seq).flatten()

#         # Métricas
#         mae = mean_absolute_error(y_val_seq, y_pred)
#         mape = calcular_mape_seguro(y_val_seq, y_pred)
#         mse = mean_squared_error(y_val_seq, y_pred)
#         rmse = np.sqrt(mse)

#         residuos = y_val_seq - y_pred
#         epsilon = 1e-10
#         residuos_safe = residuos + epsilon if np.any(residuos <= 0) else residuos

#         lb_test = acorr_ljungbox(residuos_safe, lags=[1], return_df=True)
#         p_valor = lb_test['lb_pvalue'].values[0]

#         metricas_rnn.append({
#             "Ventana": nombre_ventana,
#             "MAE": mae,
#             "MAPE": mape,
#             "MSE": mse,
#             "RMSE": rmse,
#             "p_valor_LjungBox": p_valor
#         })

#     return pd.DataFrame(metricas_rnn)

3. Evaluación de la Red Neuronal¶

In [ ]:
# df_rnn_T7 = entrenar_rnn_splits(generar_splits_temporales(df, 7, 2, selected_features=selected_features, timesteps=24), "T=7", timesteps=24)
# df_rnn_T14 = entrenar_rnn_splits(generar_splits_temporales(df, 14, 2, selected_features=selected_features, timesteps=24), "T=14", timesteps=24)
# df_rnn_T21 = entrenar_rnn_splits(generar_splits_temporales(df, 21, 2, selected_features=selected_features, timesteps=24), "T=21", timesteps=24)

# resultados_rnn = pd.concat([df_rnn_T7, df_rnn_T14, df_rnn_T21])
# promedios_rnn = resultados_rnn.groupby("Ventana").mean().reset_index()

# print("\nPromedios del modelo RNN por ventana:")
# print(promedios_rnn.round(4))

k. Modelo LSTM¶

1. Crear secuencias de tiempo¶

In [ ]:
def preparar_datos_lstm(X, y, timesteps=24):
    X_seq, y_seq = [], []
    for i in range(len(X) - timesteps):
        X_seq.append(X.iloc[i:i+timesteps].values)
        y_seq.append(y.iloc[i+timesteps])
    return np.array(X_seq), np.array(y_seq)

2. Entrenamiento del modelo¶

In [ ]:
def entrenar_lstm_splits(splits, nombre_ventana, timesteps=24, units=50, epochs=20, batch_size=32, verbose=0):
    metricas_lstm = []

    for X_train, y_train, X_val, y_val in splits:
        # Escalar
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)
        X_val_scaled = scaler.transform(X_val)

        # Reconstruir DataFrames
        X_train_df = pd.DataFrame(X_train_scaled, columns=X_train.columns)
        X_val_df = pd.DataFrame(X_val_scaled, columns=X_val.columns)

        # Convertir a secuencias
        X_train_seq, y_train_seq = preparar_datos_lstm(X_train_df, y_train.reset_index(drop=True), timesteps)
        X_val_seq, y_val_seq = preparar_datos_lstm(X_val_df, y_val.reset_index(drop=True), timesteps)

        # Modelo LSTM
        model = Sequential()
        model.add(LSTM(units, activation='tanh', input_shape=(timesteps, X_train_seq.shape[2])))
        model.add(Dense(1))
        model.compile(optimizer=Adam(), loss='mse')

        model.fit(X_train_seq, y_train_seq, epochs=epochs, batch_size=batch_size, verbose=verbose)

        y_pred = model.predict(X_val_seq).flatten()

        # Métricas
        mae = mean_absolute_error(y_val_seq, y_pred)
        mape = calcular_mape_seguro(y_val_seq, y_pred)
        mse = mean_squared_error(y_val_seq, y_pred)
        rmse = np.sqrt(mse)

        residuos = y_val_seq - y_pred
        lb_test = acorr_ljungbox(residuos, lags=[1], return_df=True)
        p_valor = lb_test['lb_pvalue'].values[0]

        metricas_lstm.append({
            "Ventana": nombre_ventana,
            "MAE": mae,
            "MAPE": mape,
            "MSE": mse,
            "RMSE": rmse,
            "p_valor_LjungBox": p_valor
        })

    return pd.DataFrame(metricas_lstm)

3. Evaluación del modelo¶

In [ ]:
df_lstm_T7 = entrenar_lstm_splits(generar_splits_temporales(df, 7, 2, selected_features=selected_features, timesteps=24), "T=7", timesteps=24, epochs=20)
df_lstm_T14 = entrenar_lstm_splits(generar_splits_temporales(df, 14, 2, selected_features=selected_features, timesteps=24), "T=14", timesteps=24, epochs=20)
df_lstm_T21 = entrenar_lstm_splits(generar_splits_temporales(df, 21, 2, selected_features=selected_features, timesteps=24), "T=21", timesteps=24, epochs=20)

resultados_lstm = pd.concat([df_lstm_T7, df_lstm_T14, df_lstm_T21])
promedios_lstm = resultados_lstm.groupby("Ventana").mean().reset_index()

print("\nPromedios del modelo LSTM por ventana:")
print(promedios_lstm.round(4))

Resultados¶

In [ ]:
# Datos por modelo y ventana
data = {
    'Modelo': ['K-NN', 'K-NN', 'K-NN',
               'Regresión Lineal', 'Regresión Lineal', 'Regresión Lineal',
               'Ridge', 'Ridge', 'Ridge',
               'Lasso', 'Lasso', 'Lasso',
               'Decision Tree', 'Decision Tree', 'Decision Tree',
               'XGBoost', 'XGBoost', 'XGBoost'],
    'Ventana': ['T=7', 'T=14', 'T=21'] * 6,
    'MAE': [0.5807, 0.5459, 0.5312,
            0.4887, 0.4749, 0.4671,
            0.4881, 0.4749, 0.4672,
            0.9915, 0.9853, 0.9772,
            0.5893, 0.5423, 0.5231,
            0.5587, 0.5282, 0.5133],
    'MAPE': [267.83, 246.30, 238.04,
             198.79, 197.68, 196.88,
             200.90, 198.73, 197.54,
             622.45, 630.25, 627.03,
             201.58, 187.87, 185.49,
             220.47, 201.32, 197.87],
    'MSE': [0.5705, 0.5067, 0.4815,
            0.6773, 0.5559, 0.3985,
            0.6643, 0.5539, 0.3985,
            1.5252, 1.4965, 1.4694,
            0.6258, 0.5308, 0.4944,
            0.5427, 0.4858, 0.4621],
    'RMSE': [0.7308, 0.6907, 0.6739,
             0.6458, 0.6203, 0.6028,
             0.6443, 0.6201, 0.6027,
             1.1928, 1.1843, 1.1741,
             1.5252, 1.4965, 1.4694,
             0.7138, 0.6766, 0.6595],
    'p_valor_LjungBox': [0.3338, 0.3757, 0.3948,
                         0.4387, 0.4468, 0.4485,
                         0.4382, 0.4463, 0.4481,
                         0.0574, 0.0597, 0.0612,
                         0.4166, 0.4322, 0.4485,
                         0.4092, 0.4332, 0.4418]
}

# Crear DataFrame
df_summary = pd.DataFrame(data)

# Mostrar tabla
display(df_summary)
Modelo Ventana MAE MAPE MSE RMSE p_valor_LjungBox
0 K-NN T=7 0.5807 267.83 0.5705 0.7308 0.3338
1 K-NN T=14 0.5459 246.30 0.5067 0.6907 0.3757
2 K-NN T=21 0.5312 238.04 0.4815 0.6739 0.3948
3 Regresión Lineal T=7 0.4887 198.79 0.6773 0.6458 0.4387
4 Regresión Lineal T=14 0.4749 197.68 0.5559 0.6203 0.4468
5 Regresión Lineal T=21 0.4671 196.88 0.3985 0.6028 0.4485
6 Ridge T=7 0.4881 200.90 0.6643 0.6443 0.4382
7 Ridge T=14 0.4749 198.73 0.5539 0.6201 0.4463
8 Ridge T=21 0.4672 197.54 0.3985 0.6027 0.4481
9 Lasso T=7 0.9915 622.45 1.5252 1.1928 0.0574
10 Lasso T=14 0.9853 630.25 1.4965 1.1843 0.0597
11 Lasso T=21 0.9772 627.03 1.4694 1.1741 0.0612
12 Decision Tree T=7 0.5893 201.58 0.6258 1.5252 0.4166
13 Decision Tree T=14 0.5423 187.87 0.5308 1.4965 0.4322
14 Decision Tree T=21 0.5231 185.49 0.4944 1.4694 0.4485
15 XGBoost T=7 0.5587 220.47 0.5427 0.7138 0.4092
16 XGBoost T=14 0.5282 201.32 0.4858 0.6766 0.4332
17 XGBoost T=21 0.5133 197.87 0.4621 0.6595 0.4418

Tras comparar todos los modelos, el mejor desempeño global se obtiene con la Regresión Lineal y Ridge Regressor con ventana T=21, ambos con los menores errores (MAE ≈ 0.467, RMSE ≈ 0.602) y residuos no autocorrelacionados (p > 0.44). También destacan por tener los MAPE más bajos.

Modelos como K-NN, Decision Tree y XGBoost funcionan adecuadamente, pero son menos precisos. El modelo Lasso, en cambio, tuvo un rendimiento significativamente inferior, con errores y MAPE muy elevados, y residuos posiblemente autocorrelacionados. En resumen, el modelo lineal regularizado con T=21 es la opción más robusta y precisa para esta tarea.